blob: 31d324d5905f90f3627fa61513b0298457f49bd0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019 Andrey Loskutov and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Andrey Loskutov - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.core.tests.builder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.tests.util.Util;
/**
* Base class for testing builder related opened file leak tests, see bug 543506
*/
public abstract class AbstractLeakTest extends BuilderTests {
static boolean WINDOWS;
static boolean LINUX;
static boolean MAC;
static {
String os = System.getProperty("os.name").toLowerCase();
WINDOWS = os.contains("windows");
LINUX = os.contains("linux");
MAC = os.contains("mac");
}
public AbstractLeakTest(String name) {
super(name);
}
protected void testLeaksOnIncrementalBuild() throws Exception {
if(MAC) {
return;
}
internalTestUsedLibraryLeaks(IncrementalProjectBuilder.INCREMENTAL_BUILD);
}
protected void testLeaksOnCleanBuild() throws Exception {
if(MAC) {
return;
}
internalTestUsedLibraryLeaks(IncrementalProjectBuilder.CLEAN_BUILD);
}
protected void testLeaksOnFullBuild() throws Exception {
if(MAC) {
return;
}
internalTestUsedLibraryLeaks(IncrementalProjectBuilder.FULL_BUILD);
}
private void internalTestUsedLibraryLeaks(int kind) throws Exception {
String projectName = getName();
IPath projectPath = env.addProject(projectName, getCompatibilityLevel());
env.setOutputFolder(projectPath, "");
env.addExternalJars(projectPath, Util.getJavaClassLibs());
IPath internalJar = addInternalJar(projectPath);
createJavaFile(projectPath);
switch (kind) {
case IncrementalProjectBuilder.CLEAN_BUILD:
cleanBuild(projectName);
assertNotLeaked(internalJar);
break;
case IncrementalProjectBuilder.FULL_BUILD:
fullBuild(projectPath);
assertNotLeaked(internalJar);
break;
case IncrementalProjectBuilder.INCREMENTAL_BUILD:
incrementalBuild(projectPath);
changeJavaFile(projectPath);
incrementalBuild(projectPath);
assertNotLeaked(internalJar);
break;
default:
fail("Unexpected build kind: " + kind);
}
}
abstract String getCompatibilityLevel();
private IPath addInternalJar(IPath projectPath) throws IOException, JavaModelException {
IPath internalJar = addEmptyInternalJar(projectPath, "test.jar");
return internalJar;
}
private void createJavaFile(IPath projectPath) {
IPath path = env.addClass(projectPath, "a", "Other",
"package a;\n" +
"public class Other {\n" +
"}"
);
IFile file = env.getWorkspace().getRoot().getFile(path);
assertTrue("File should exists: " + path, file.exists());
}
private void changeJavaFile(IPath projectPath) throws Exception {
IPath path = env.addClass(projectPath, "a", "Other",
"package a;\n" +
"public class Other {\n" +
" // an extra comment \n" +
"}"
);
IFile file = env.getWorkspace().getRoot().getFile(path);
assertTrue("FIle should exists: " + path, file.exists());
}
private void assertNotLeaked(IPath path) throws Exception {
expectingNoProblems();
IFile file = env.getWorkspace().getRoot().getFile(path);
assertTrue("FIle should exists: " + path, file.exists());
if(WINDOWS) {
tryRemoveFile(file);
} else if (LINUX) {
checkOpenDescriptors(file);
}
}
private void tryRemoveFile(IFile file) {
// Note: this is a lame attempt to check for leaked file descriptor
// This works on Windows only, because windows does not allow to delete
// files opened for reading.
// On Linux we need something like lsof -p <my_process_id> | grep file name
try {
file.delete(true, null);
} catch (CoreException e) {
try {
// second attempt to avoid delays on teardown
Files.deleteIfExists(file.getLocation().toFile().toPath());
} catch (Exception e2) {
file.getLocation().toFile().delete();
// ignore
}
throw new IllegalStateException("File leaked during build: " + file, e);
}
assertFalse("File should be deleted: " + file, file.exists());
}
private void checkOpenDescriptors(IFile file) throws Exception {
List<String> openDescriptors = getOpenDescriptors();
assertFalse("Failed to read opened file descriptors", openDescriptors.isEmpty());
if(openDescriptors.contains(file.getLocation().toOSString())) {
throw new IllegalStateException("File leaked during build: " + file);
}
}
private static List<String> getOpenDescriptors() throws Exception {
int pid = getPid();
assertTrue("JVM PID must be > 0 : " + pid, pid > 0);
// -F n : to print only name column (note: all lines start with "n")
// -a : to "and" all following options
// -b :to avoid blocking calls
// -p <pid>: to select process with opened files
List<String> lines = readLsofLines("lsof -F n -a -p " + pid + " / -b ", true);
return lines;
}
private static int getPid() throws Exception {
String jvmName = ManagementFactory.getRuntimeMXBean().getName();
int indexOfAt = jvmName.indexOf('@');
String pidSubstring = jvmName.substring(0, indexOfAt);
int pid = Integer.valueOf(pidSubstring);
return pid;
}
private static List<String> readLsofLines(String cmd, boolean skipFirst) throws Exception {
List<String> lines = new ArrayList<>();
Process process = Runtime.getRuntime().exec(cmd);
try (BufferedReader rdr = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
if (skipFirst) {
rdr.readLine();
}
String line;
while((line = rdr.readLine())!= null) {
// remove "n" prefix from lsof output
if(line.startsWith("n")) {
line = line.substring(1);
}
if(line.trim().length() > 1) {
lines.add(line);
}
}
}
lines.sort(null);
return lines;
}
}