package tests;
import static org.junit.Assert.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import de.tobject.findbugs.FindbugsPlugin;
import de.tobject.findbugs.builder.FindBugsWorker;
import de.tobject.findbugs.builder.WorkItem;
import de.tobject.findbugs.reporter.MarkerUtil;
import edu.umd.cs.findbugs.DetectorFactory;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.Plugin;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import edu.umd.cs.findbugs.config.UserPreferences;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.BugResolutionGenerator;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.testplugin.JavaProjectHelper;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.ui.IMarkerResolution;
import org.eclipse.ui.PartInitException;
import org.junit.Assume;
import utils.BugResolutionSource;
import utils.QuickFixTestPackage;
import utils.TestingUtils;
public abstract class TestHarness {
public static final String PROJECT_NAME = "fb-contrib-test-quick-fixes";
public static final String BIN_FOLDER_NAME = "bin";
public static final String SRC_FOLDER_NAME = "src";
private static IJavaProject testProject;
private static IProject testIProject;
private static boolean fb_contribInstalled;
private static boolean findSecurityBugsInstalled;
private BugResolutionSource resolutionSource;
private Set<String> detectorsToReenable = new HashSet<String>();
public static void loadFilesThatNeedFixing() throws CoreException, IOException {
makeJavaProject();
TestingUtils.copyBrokenFiles(testIProject.getFolder(SRC_FOLDER_NAME), new File("classesToFix/"), new File("mockLibraries/"));
// Compiles the code
testIProject.refreshLocal(IResource.DEPTH_INFINITE, null);
testIProject.build(IncrementalProjectBuilder.FULL_BUILD, null);
FindbugsPlugin.setProjectSettingsEnabled(testIProject, null, true);
UserPreferences userPrefs = FindbugsPlugin.getUserPreferences(testIProject);
// enables categories like Security, which are disabled by default
userPrefs.getFilterSettings().clearAllCategories();
checkFBContribInstalled();
checkFindSecurityBugsInstalled();
TestingUtils.waitForUiEvents(100);
}
private static void checkFBContribInstalled() {
fb_contribInstalled = Plugin.getByPluginId("com.mebigfatguy.fbcontrib") != null;
System.out.printf("fb-contrib (com.mebigfatguy.fbcontrib) is %sinstalled%n", fb_contribInstalled ? "": "not ");
}
protected void needsFBContrib() { //convenience
needsFBContrib(true);
}
protected void needsFBContrib(boolean isNeeded) {
if (isNeeded || fb_contribInstalled) {
Assume.assumeTrue(fb_contribInstalled);
enablePlugins("com.mebigfatguy.fbcontrib", isNeeded);
}
}
private static void checkFindSecurityBugsInstalled() {
findSecurityBugsInstalled = Plugin.getByPluginId("com.h3xstream.findsecbugs") != null;
System.out.printf("Find Security Bugs (com.h3xstream.findsecbugs) is %sinstalled%n", findSecurityBugsInstalled ? "": "not ");
}
protected void needsFindSecurityBugs() {
needsFindSecurityBugs(true);
}
protected void needsFindSecurityBugs(boolean isNeeded) {
if (isNeeded || findSecurityBugsInstalled) {
Assume.assumeTrue(findSecurityBugsInstalled);
enablePlugins("com.h3xstream.findsecbugs", isNeeded);
}
}
private static void clearMarkersAndBugs() throws CoreException {
MarkerUtil.removeMarkers(testIProject);
FindbugsPlugin.getBugCollection(testIProject, null, false).clearBugInstances();
}
private static void makeJavaProject() throws CoreException {
testProject = JavaProjectHelper.createJavaProject(PROJECT_NAME, BIN_FOLDER_NAME);
JavaProjectHelper.addRTJar17(testProject);
JavaProjectHelper.addSourceContainer(testProject, SRC_FOLDER_NAME);
testProject.setOption("org.eclipse.jdt.core.formatter.tabulation.char", JavaCore.SPACE);
testIProject = testProject.getProject();
}
public void setup() {
detectorsToReenable.clear();
final BugResolutionGenerator resolutionGenerator = new BugResolutionGenerator();
// we wrap this in case the underlying generator interface changes.
resolutionSource = new BugResolutionSource() {
@Override
public IMarkerResolution[] getResolutions(IMarker marker) {
return resolutionGenerator.getResolutions(marker);
}
@Override
public boolean hasResolutions(IMarker marker) {
return resolutionGenerator.hasResolutions(marker);
}
};
}
protected void checkBugsAndPerformResolution(List<QuickFixTestPackage> packages, String testResource) {
try {
showEditorWindowForFile(testResource);
scanUntilMarkers(testResource);
assertBugPatternsMatch(packages, testResource);
executeResolutions(packages, testResource);
assertOutputAndInputFilesMatch(testResource);
} catch (CoreException | IOException e) {
e.printStackTrace();
fail("Exception thrown while performing resolution on " + testResource);
}
}
private void scanUntilMarkers(String testResource) throws CoreException {
scanForBugs(testResource);
for (int i = 0; i < 3; i++) {
IMarker[] markers = TestingUtils.getAllMarkersInResource(testProject, testResource);
if (markers.length > 0) {
return;
}
System.out.println("Trying again for bugs...");
clearMarkersAndBugs();
scanForBugs(testResource);
TestingUtils.waitForUiEvents(1000);
}
fail("Did not find any markers... tried 3 times");
}
private void showEditorWindowForFile(String testResource) throws JavaModelException, PartInitException {
JavaUI.openInEditor(TestingUtils.elementFromProject(testProject, testResource), true, true);
}
/**
* Blocks until FindBugs has finished scanning
*
* @param className
* file in this project to scan
* @throws CoreException
*/
private void scanForBugs(String className) throws CoreException {
IJavaElement element = testProject.findElement(new Path(className));
if (element == null)
{
fail("Could not find java class " + className);
return;
}
final AtomicBoolean isWorking = new AtomicBoolean(true);
FindBugsWorker worker = new FindBugsWorker(testProject.getProject(), new NullProgressMonitor() {
@Override
public void done() {
isWorking.set(false);
}
});
worker.work(Collections.singletonList(new WorkItem(element)));
// wait for the findBugsWorker to finish
// 500ms reduces the chance that the IMarkers haven't loaded yet and the tests will fail unpredictably
// (see JavaProjectHelper discussion about performDummySearch for more info)
TestingUtils.waitForUiEvents(800);
while (isWorking.get()) {
TestingUtils.waitForUiEvents(100);
}
}
private void assertBugPatternsMatch(List<QuickFixTestPackage> packages, String testResource) throws JavaModelException {
IMarker[] markers = TestingUtils.getAllMarkersInResource(testProject, testResource);
TestingUtils.sortMarkersByPatterns(markers);
// packages and markers should now be lined up to match up one to one.
assertEquals("The number of markers is wrong, check your turned off detectors?",
packages.size(), markers.length);
TestingUtils.assertBugPatternsMatch(packages, markers);
TestingUtils.assertLabelsAndDescriptionsMatch(packages, markers, resolutionSource);
TestingUtils.assertLineNumbersMatch(packages, markers);
TestingUtils.assertAllMarkersHaveResolutions(markers, resolutionSource);
}
private void executeResolutions(List<QuickFixTestPackage> packages, String testResource) throws CoreException {
// Some resolutions can be ignored. One example is a detector that has one or more sometimes
// applicable quickfixes, but occasionally none apply. These are useful to include in the
// test cases (e.g. DeadLocalStoreBugs.java), and need to be properly handled.
// we keep a count of the ignored resolutions (QuickFixTestPackage.IGNORE_FIX) and correct
// our progress using that
int ignoredResolutions = 0;
int pendingBogoFixes = 0;
boolean skipNextScan = true;
for (int resolutionsCompleted = 0; resolutionsCompleted < packages.size(); resolutionsCompleted++) {
if (!skipNextScan) { // Refresh, rebuild, and scan for bugs again
// We only need to do this after the first time, as we expect the file to have
// been scanned and checked for consistency (see checkBugsAndPerformResolution)
testIProject.refreshLocal(IResource.DEPTH_ONE, null);
testIProject.build(IncrementalProjectBuilder.FULL_BUILD, null);
clearMarkersAndBugs();
scanUntilMarkers(testResource);
}
skipNextScan = false;
System.out.println(resolutionsCompleted);
IMarker[] markers = getSortedMarkersFromFile(testResource);
assertEquals("Bug marker number was different than anticipated. "
+ "Check to see if another bug marker was introduced by fixing another.",
packages.size() - (resolutionsCompleted),
markers.length - ignoredResolutions - pendingBogoFixes);
IMarker nextNonIgnoredMarker = markers[ignoredResolutions + pendingBogoFixes]; // Bug markers we ignore float to the "top" of the stack
// ignoredResolutions can act as an index for that
QuickFixTestPackage p = packages.get(resolutionsCompleted);
skipNextScan = !performResolution(p, nextNonIgnoredMarker);
if (p.resolutionToExecute == QuickFixTestPackage.IGNORE_FIX) {
ignoredResolutions++;
} else if (p.resolutionToExecute == QuickFixTestPackage.FIXED_BY_ANOTHER_FIX) {
pendingBogoFixes++;
}
else {
pendingBogoFixes = 0;
}
}
}
private IMarker[] getSortedMarkersFromFile(String fileName) throws JavaModelException {
IJavaElement fileToScan = TestingUtils.elementFromProject(testProject, fileName);
IMarker[] markers = TestingUtils.getAllMarkersInResource(fileToScan.getCorrespondingResource());
// the markers get sorted first by bug name, then by line number, just like
// the QuickFixTestPackages, so we have a reliable way to fix them
// so we can assert properties more accurately.
TestingUtils.sortMarkersByPatterns(markers);
return markers;
}
private boolean performResolution(QuickFixTestPackage qfPackage, IMarker marker) {
if (qfPackage.resolutionToExecute < 0) {
return false; // false means the marker should be ignored.
}
// This doesn't actually click on the bug marker, but it programmatically
// gets the same resolutions.
IMarkerResolution[] resolutions = resolutionSource.getResolutions(marker);
assertTrue("I wanted to execute resolution #" + qfPackage.resolutionToExecute + " of " + qfPackage +
" but there were only " + resolutions.length + " to choose from."
, resolutions.length > qfPackage.resolutionToExecute);
// the order isn't guaranteed, so we have to check the labels.
@SuppressFBWarnings("NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD")
String resolutionToDo = qfPackage.expectedLabels.get(qfPackage.resolutionToExecute);
for (IMarkerResolution resolution : resolutions) {
if (resolution.getLabel().equals(resolutionToDo)) {
resolution.run(marker);
}
}
return true; // a resolution was performed and we expect the bug marker to disappear.
}
private void assertOutputAndInputFilesMatch(String testResource) throws JavaModelException, IOException {
URL expectedFile = getClass().getResource("/fixedClasses/"+testResource);
IJavaElement actualFile = TestingUtils.elementFromProject(testProject, testResource);
TestingUtils.assertOutputAndInputFilesMatch(expectedFile, actualFile);
}
/**
* Enables or disables a FindBugs plugin.
*
* @param plugins
* @param enabled
*/
protected void enablePlugins(String pluginId, boolean enabled) {
Plugin plugin = Plugin.getByPluginId(pluginId);
if (plugin != null) {
if (!enabled && plugin.cannotDisable()) {
fail("Cannot disable plugin: " + plugin.getPluginId() + '\n' + plugin.getShortDescription());
} else {
plugin.setGloballyEnabled(enabled);
}
}
}
/**
* Enables or disables a detector. This can only disable an entire detector (class-level),
* not just one bug pattern.
*
* @param dotSeperatedDetectorClass
* A string with the dot-separated-class of the detector to enable/disable
* @param enabled
*/
protected void setDetector(String dotSeperatedDetectorClass, boolean enabled) {
DetectorFactory factory = DetectorFactoryCollection.instance().getFactoryByClassName(dotSeperatedDetectorClass);
if (factory == null) {
if (enabled) {
fail("Could not find a detector with class " + dotSeperatedDetectorClass);
} else {
System.err.println("Could not find a detector with class " + dotSeperatedDetectorClass);
}
return;
}
if (!enabled) {
detectorsToReenable.add(dotSeperatedDetectorClass);
}
FindbugsPlugin.getUserPreferences(testIProject).enableDetector(factory, enabled);
}
private void restoreDisabledDetectors() {
for (String detector : detectorsToReenable) {
setDetector(detector, true);
}
}
/**
* Set minimum warning priority threshold to be used for scanning
*
* Project defaults to "Medium"
*
* @param minPriority
* the priority threshold: one of "High", "Medium", or "Low"
*/
protected void setPriority(String minPriority) {
// short hand for seeing if minPriority is one of High, Medium or Low
// Each of the three valid strings are padded to be 6 chars long with spaces
// if minPriority is one of the valid strings, the index will be 0, 6 or 12
// it's about 40% slower than either doing a set or an explicit 3- case, but takes
// less than 1ms for 1000 iterations (in any event)
if ("High MediumLow ".indexOf(minPriority.trim()) % 6 == 0) {
FindbugsPlugin.getProjectPreferences(testIProject, false).getFilterSettings().setMinPriority(minPriority);
return;
}
fail("minPriority [" + minPriority + "] must be one of \"High\", \"Medium\", or \"Low\"");
}
/**
* Set minimum bugRank to show up for scanning
*
* Project defaults to 15
*
* @param minPriority
* the priority threshold: one of "High", "Medium", or "Low"
*/
protected void setRank(int minRank) {
if (minRank >= 1 && minRank <= 20) {
FindbugsPlugin.getProjectPreferences(testIProject, false).getFilterSettings().setMinRank(minRank);
return;
}
fail("minRank [" + minRank + "] must be between 1 and 20 inclusively");
}
public void tearDown() {
restoreDisabledDetectors();
}
}