blob: 2fde7f49ae68b3c8f1d4d9d7c5b3b12e3730afa8 [file] [log] [blame]
// Copyright 2017 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.android.desugar.runtime;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.testing.GcFinalization;
import com.google.devtools.build.android.desugar.runtime.ThrowableExtension.ConcurrentWeakIdentityHashMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Test for {@link ConcurrentWeakIdentityHashMap}. This test uses multi-threading, and needs GC
* sometime to assert weak references, so it could take long.
*/
@RunWith(JUnit4.class)
public class ConcurrentWeakIdentityHashMapTest {
private final Random random = new Random();
/**
* This method makes sure that after return, all the exceptions in the map should be garbage
* collected. .
*/
private static ConcurrentWeakIdentityHashMap
testConcurrentWeakIdentityHashMapSingleThreadedHelper(CountDownLatch latch)
throws InterruptedException {
ConcurrentWeakIdentityHashMap map = new ConcurrentWeakIdentityHashMap();
Exception e1 = new ExceptionWithLatch("e1", latch);
assertThat(map.get(e1, false)).isNull();
assertThat(map.get(e1, true)).isNotNull();
assertThat(map.get(e1, true)).isEmpty();
assertThat(map.get(e1, false)).isNotNull();
Exception suppressed1 = new ExceptionWithLatch("suppressed1", latch);
map.get(e1, true).add(suppressed1);
assertThat(map.get(e1, true)).containsExactly(suppressed1);
Exception suppressed2 = new ExceptionWithLatch("suppressed2", latch);
map.get(e1, true).add(suppressed2);
assertThat(map.get(e1, true)).containsExactly(suppressed1, suppressed2);
assertThat(map.get(suppressed1, false)).isNull();
assertThat(map.get(suppressed2, false)).isNull();
assertThat(map.size()).isEqualTo(1);
assertThat(map.get(suppressed1, true)).isNotNull();
assertThat(map.size()).isEqualTo(2);
assertThat(map.get(suppressed1, true)).isNotNull();
assertThat(map.size()).isEqualTo(2);
Exception e2 = new ExceptionWithLatch("e2", latch);
assertThat(map.get(e2, true)).isNotNull();
Exception e3 = new ExceptionWithLatch("e3", latch);
assertThat(map.get(e3, true)).isNotNull();
assertThat(map.size()).isEqualTo(4);
return map;
}
@Test
public void testSingleThreadedUse() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(5);
ConcurrentWeakIdentityHashMap map =
testConcurrentWeakIdentityHashMapSingleThreadedHelper(latch);
for (int i = 0; i < 5; i++) {
map.deleteEmptyKeys();
GcFinalization.awaitFullGc();
}
latch.await(); // wait for e1 to be garbage collected.
map.deleteEmptyKeys();
assertThat(map.size()).isEqualTo(0);
}
private static Map<Throwable, List<Throwable>> createExceptionWithSuppressed(
int numMainExceptions, int numSuppressedPerMain, CountDownLatch latch) {
Map<Throwable, List<Throwable>> map = new HashMap<>();
for (int i = 0; i < numMainExceptions; ++i) {
Exception main = new ExceptionWithLatch("main-" + i, latch);
List<Throwable> suppressedList = new ArrayList<>();
assertThat(map).doesNotContainKey(main);
map.put(main, suppressedList);
for (int j = 0; j < numSuppressedPerMain; ++j) {
Exception suppressed = new ExceptionWithLatch("suppressed-" + j + "-main-" + i, latch);
suppressedList.add(suppressed);
}
}
return map;
}
private ConcurrentWeakIdentityHashMap testFunctionalCorrectnessForMultiThreadedUse(
int numMainExceptions, int numSuppressedPerMain, CountDownLatch latch)
throws InterruptedException {
Map<Throwable, List<Throwable>> exceptionWithSuppressed =
createExceptionWithSuppressed(numMainExceptions, numSuppressedPerMain, latch);
assertThat(exceptionWithSuppressed).hasSize(numMainExceptions);
List<Pair> allPairs =
exceptionWithSuppressed
.entrySet()
.stream()
.flatMap(
entry -> entry.getValue().stream().map(value -> new Pair(entry.getKey(), value)))
.collect(Collectors.toList());
Collections.shuffle(allPairs);
ConcurrentWeakIdentityHashMap map = new ConcurrentWeakIdentityHashMap();
List<Worker> workers =
IntStream.range(1, 11) // ten threads.
.mapToObj(i -> new Worker("worker-" + i, map))
.collect(Collectors.toList());
// Assign tasks to workers.
Iterator<Worker> workIterator = workers.iterator();
for (Pair pair : allPairs) {
if (!workIterator.hasNext()) {
workIterator = workers.iterator();
}
assertThat(workIterator.hasNext()).isTrue();
workIterator.next().exceptionList.add(pair);
}
// Execute all the workers.
ExecutorService executorService = Executors.newFixedThreadPool(workers.size());
workers.forEach(executorService::execute);
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); // wait for completion.
exceptionWithSuppressed
.entrySet()
.forEach(
entry -> {
assertThat(map.get(entry.getKey(), false)).isNotNull();
assertThat(map.get(entry.getKey(), false))
.containsExactlyElementsIn(exceptionWithSuppressed.get(entry.getKey()));
});
return map;
}
private void testMultiThreadedUse(int numMainExceptions, int numSuppressedPerMain)
throws InterruptedException {
CountDownLatch latch = new CountDownLatch(numMainExceptions * numSuppressedPerMain);
ConcurrentWeakIdentityHashMap map =
testFunctionalCorrectnessForMultiThreadedUse(
numMainExceptions, numSuppressedPerMain, latch);
/*
* Calling the following methods multiple times to make sure the keys are garbage collected,
* and their corresponding entries are removed from the map.
*/
map.deleteEmptyKeys();
GcFinalization.awaitFullGc();
map.deleteEmptyKeys();
GcFinalization.awaitFullGc();
map.deleteEmptyKeys();
assertThat(map.size()).isEqualTo(0);
}
@Test
public void testMultiThreadedUseMedium() throws InterruptedException {
for (int i = 0; i < 10; ++i) {
testMultiThreadedUse(50, 100);
}
}
@Test
public void testMultiThreadedUseLarge() throws InterruptedException {
for (int i = 0; i < 5; ++i) {
testMultiThreadedUse(100, 100);
}
}
@Test
public void testMultiThreadedUseSmall() throws InterruptedException {
for (int i = 0; i < 10; ++i) {
testMultiThreadedUse(20, 100);
}
}
private static class ExceptionWithLatch extends Exception {
private final CountDownLatch latch;
private ExceptionWithLatch(String message, CountDownLatch latch) {
super(message);
this.latch = latch;
}
@Override
public String toString() {
return this.getMessage();
}
@Override
protected void finalize() throws Throwable {
latch.countDown();
}
}
private static class Pair {
final Throwable throwable;
final Throwable suppressed;
public Pair(Throwable throwable, Throwable suppressed) {
this.throwable = throwable;
this.suppressed = suppressed;
}
}
private class Worker implements Runnable {
private final ConcurrentWeakIdentityHashMap map;
private final List<Pair> exceptionList = new ArrayList<>();
private final String name;
private Worker(String name, ConcurrentWeakIdentityHashMap map) {
this.name = name;
this.map = map;
}
public String getName() {
return name;
}
@Override
public void run() {
Iterator<Pair> iterator = exceptionList.iterator();
while (iterator.hasNext()) {
int timeToSleep = random.nextInt(3);
if (random.nextBoolean() && timeToSleep > 0) {
try {
Thread.sleep(timeToSleep); // add randomness to the scheduler.
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
Pair pair = iterator.next();
List<Throwable> suppressed = map.get(pair.throwable, true);
System.out.printf("add suppressed %s to %s\n", pair.suppressed, pair.throwable);
suppressed.add(pair.suppressed);
}
}
}
}