/*******************************************************************************
* Copyright (c) 2015 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import junit.framework.AssertionFailedError;
import org.eclipse.core.internal.events.NotificationManager;
import org.eclipse.core.internal.events.ResourceChangeListenerList;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.debug.core.DebugPlugin;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition;
/**
* JUnit 4 'Rule' that checks whether a test registered some
* listeners but didn't deregister them. This is often an indication
* of resource/memory leak.
*
* @author Kris De Volder
*/
@SuppressWarnings("restriction")
public class ListenerLeakDetector implements TestRule {
private Set<Object> startingListeners;
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
start();
base.evaluate();
verify();
}
};
}
protected void start() throws Exception {
startingListeners = getListeners();
}
@SuppressWarnings("unchecked")
protected Set<Object> getListeners() throws Exception {
Set<Object> listeners = new HashSet<>();
listeners.addAll(getDebugListeners());
listeners.addAll(getWorkspaceListeners());
return listeners;
}
protected List<Object> getDebugListeners() throws Exception {
DebugPlugin plugin = DebugPlugin.getDefault();
Field f = DebugPlugin.class.getDeclaredField("fEventListeners");
f.setAccessible(true);
ListenerList list = (ListenerList)f.get(plugin);
List<Object> listeners = new ArrayList<>();
for (Object l : list.getListeners()) {
if (isInteresting(l)) {
listeners.add(l);
}
}
return listeners;
}
protected List<Object> getWorkspaceListeners() throws Exception {
IWorkspace workspace = ResourcesPlugin.getWorkspace();
NotificationManager notMan = (NotificationManager) getField(workspace, "notificationManager");
ResourceChangeListenerList listenerList = (ResourceChangeListenerList)getField(notMan, "listeners");
Object[] entries = listenerList.getListeners(); //Watch out, these entries aren't the actual
// listeners yet.
if (entries!=null) {
List<Object> listeners = new ArrayList<>(entries.length);
for (int i = 0; i < entries.length; i++) {
Object l = getField(entries[i], "listener");
if (isInteresting(l)) {
listeners.add(l);
}
}
return listeners;
}
return Collections.emptyList();
}
/**
* Eclipse is one noisy bugger when it comes to adding workspace listeners. We really don't care
* about the listener classes we don't own/control for this leak detection... so...
*/
protected boolean isInteresting(Object l) {
String classname = l.getClass().getName();
return classname.startsWith("org.springframework.ide.eclipse.boot") && !isIgnoredListenerClassName(classname);
}
private boolean isIgnoredListenerClassName(String classname) {
//Phill's plugin attaches listeners when console ui kicks in. Ignore those, they are only disposed if someone closes / disposes the
// ui associated with the console.
return classname.startsWith("org.springframework.ide.eclipse.boot.restart.RestartConsolePageParticipant");
}
private Object getField(Object self, String name) throws Exception {
Field f = self.getClass().getDeclaredField(name);
f.setAccessible(true);
return f.get(self);
}
protected void verify() throws Throwable {
ACondition.waitFor("listeners removed", 2000, () -> {
Set<Object> endingListeners = getListeners();
for (Object l : endingListeners) {
if (!startingListeners.contains(l)) {
throw new AssertionFailedError("Leaked listener: "+l+" of class "+l.getClass().getName());
}
}
});
}
}