ability to specify external test discovery index

(cherry picked from commit 36c0752)
This commit is contained in:
Maxim.Mossienko
2016-06-15 16:08:41 +03:00
committed by Anna Kozlova
parent 2bae41452f
commit 8e6e6c360f
2 changed files with 667 additions and 500 deletions
@@ -22,19 +22,22 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.newvfs.persistent.FlushingDaemon;
import com.intellij.util.io.*;
import com.intellij.util.ThrowableConvertor;
import com.intellij.util.io.DataInputOutputUtil;
import com.intellij.util.io.IOUtil;
import gnu.trove.THashSet;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntHashSet;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.io.DataOutputStream;
import java.util.*;
import java.util.concurrent.ScheduledFuture;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @author Maxim.Mossienko on 7/9/2015.
@@ -42,11 +45,11 @@ import java.util.concurrent.ScheduledFuture;
public class TestDiscoveryIndex implements ProjectComponent {
static final Logger LOG = Logger.getInstance(TestDiscoveryIndex.class);
private static final int REMOVED_MARKER = -1;
private final Object ourLock = new Object();
private final Project myProject;
private final String myBasePath;
private volatile Holder myHolder;
//private volatile TestInfoHolder mySystemHolder;
private final TestDataController myLocalTestRunDataController;
private final TestDataController myRemoteTestRunDataController;
public TestDiscoveryIndex(Project project) {
this(project, TestDiscoveryExtension.baseTestDiscoveryPathForProject(project));
@@ -55,76 +58,130 @@ public class TestDiscoveryIndex implements ProjectComponent {
public TestDiscoveryIndex(final Project project, final String basePath) {
myProject = project;
myBasePath = basePath;
myLocalTestRunDataController = new TestDataController(basePath, false);
myRemoteTestRunDataController = new TestDataController(null, true);
if (new File(basePath).exists()) {
StartupManager.getInstance(project).registerPostStartupActivity(() -> ApplicationManager.getApplication().executeOnPooledThread(() -> {
getHolder(); // proactively init with maybe io costly compact
myLocalTestRunDataController.getHolder(); // proactively init with maybe io costly compact
}));
}
//{
// setRemoteTestRunDataPath("C:\\ultimate\\system\\testDiscovery\\145.save");
//}
}
public boolean hasTestTrace(@NotNull String testName) throws IOException {
synchronized (ourLock) {
Holder holder = null;
try {
holder = getHolder();
final int testNameId = holder.myTestNameEnumerator.tryEnumerate(testName);
if (testNameId == 0) return false;
return holder.myTestNameToUsedClassesAndMethodMap.get(testNameId) != null;
} catch (Throwable throwable) {
thingsWentWrongLetsReinitialize(holder, throwable);
return false;
Boolean result = myLocalTestRunDataController.withTestDataHolder(localHolder -> { // todo: remote run data
final int testNameId = localHolder.myTestNameEnumerator.tryEnumerate(testName);
if (testNameId == 0) {
return myRemoteTestRunDataController.withTestDataHolder(remoteHolder -> {
final int testNameId1 = remoteHolder.myTestNameEnumerator.tryEnumerate(testName);
return testNameId1 != 0 && remoteHolder.myTestNameToUsedClassesAndMethodMap.get(testNameId1) != null;
}) != null;
}
}
return localHolder.myTestNameToUsedClassesAndMethodMap.get(testNameId) != null;
});
return result == Boolean.TRUE;
}
public void removeTestTrace(@NotNull String testName) throws IOException {
synchronized (ourLock) {
Holder holder = null;
try {
holder = getHolder();
final int testNameId = holder.myTestNameEnumerator.tryEnumerate(testName);
if (testNameId == 0) return;
doUpdateFromDiff(holder, testNameId, null,
holder.myTestNameToUsedClassesAndMethodMap.get(testNameId),
null);
} catch (Throwable throwable) {
thingsWentWrongLetsReinitialize(holder, throwable);
myLocalTestRunDataController.withTestDataHolder(new ThrowableConvertor<TestInfoHolder, Void, IOException>() {
@Override
public Void convert(TestInfoHolder localHolder) throws IOException {
final int testNameId = localHolder.myTestNameEnumerator.tryEnumerate(testName); // todo remove remote data isn't possible
if (testNameId != 0) {
localHolder.doUpdateFromDiff(testNameId, null,
localHolder.myTestNameToUsedClassesAndMethodMap.get(testNameId),
null);
}
return null;
}
}
});
}
public void setRemoteTestRunDataPath(String path) {
myRemoteTestRunDataController.init(path);
// todo: should we remove our local run data ?
}
public Collection<String> getTestsByMethodName(@NotNull String classFQName, @NotNull String methodName) throws IOException {
synchronized (ourLock) {
Holder holder = null;
try {
holder = getHolder();
final TIntArrayList list = holder.myMethodQNameToTestNames.get(
createKey(
holder.myClassEnumerator.enumerate(classFQName),
holder.myMethodEnumerator.enumerate(methodName)
return myLocalTestRunDataController.withTestDataHolder(new ThrowableConvertor<TestInfoHolder, Collection<String>, IOException>() {
@Override
public Collection<String> convert(TestInfoHolder localHolder) throws IOException {
TIntArrayList remoteList = myRemoteTestRunDataController.withTestDataHolder(
remoteHolder -> remoteHolder.myMethodQNameToTestNames.get(
TestInfoHolder.createKey(
remoteHolder.myClassEnumerator.enumerate(classFQName),
remoteHolder.myMethodEnumerator.enumerate(methodName)
)
)
);
if (list == null) return Collections.emptyList();
final ArrayList<String> result = new ArrayList<String>(list.size());
for (int testNameId : list.toNativeArray()) result.add(holder.myTestNameEnumerator.valueOf(testNameId));
return result;
} catch (Throwable throwable) {
thingsWentWrongLetsReinitialize(holder, throwable);
return Collections.emptyList();
final TIntArrayList localList = localHolder.myMethodQNameToTestNames.get(
TestInfoHolder.createKey(
localHolder.myClassEnumerator.enumerate(classFQName),
localHolder.myMethodEnumerator.enumerate(methodName)
)
);
if (remoteList == null) {
return testIdsToTestNames(localList, localHolder);
}
Collection<String> testsFromRemote =
myRemoteTestRunDataController.withTestDataHolder(
remoteHolder -> testIdsToTestNames(remoteList, remoteHolder)
);
if (localList == null) return testsFromRemote;
THashSet<String> setOfStrings = new THashSet<>(testsFromRemote);
for (int testNameId : localList.toNativeArray()) {
if (testNameId < 0) {
setOfStrings.remove(localHolder.myTestNameEnumerator.valueOf(-testNameId));
continue;
}
setOfStrings.add(localHolder.myTestNameEnumerator.valueOf(testNameId));
}
return setOfStrings;
}
}
private Collection<String> testIdsToTestNames(TIntArrayList localList, TestInfoHolder localHolder) throws IOException {
if (localList == null) return Collections.emptyList();
final ArrayList<String> result = new ArrayList<String>(localList.size());
for (int testNameId : localList.toNativeArray()) {
if (testNameId < 0) {
int a = 1;
continue;
}
result.add(localHolder.myTestNameEnumerator.valueOf(testNameId));
}
return result;
}
});
}
public Collection<String> getTestModulesByMethodName(@NotNull String classFQName, @NotNull String methodName, String prefix) throws IOException {
synchronized (ourLock) {
Holder holder = null;
try {
holder = getHolder();
return myLocalTestRunDataController.withTestDataHolder(new ThrowableConvertor<TestInfoHolder, Collection<String>, IOException>() {
@Override
public Collection<String> convert(TestInfoHolder localHolder) throws IOException {
List<String> modules = getTestModules(localHolder);
List<String> modulesFromRemote = myRemoteTestRunDataController.withTestDataHolder(
this::getTestModules);
THashSet<String> modulesSet = new THashSet<>(modules);
if (modulesFromRemote != null) modulesSet.addAll(modulesFromRemote);
return modulesSet;
}
private List<String> getTestModules(TestInfoHolder holder) throws IOException {
// todo merging with remote
final TIntArrayList list = holder.myTestNameToNearestModule.get(
createKey(
TestInfoHolder.createKey(
holder.myClassEnumerator.enumerate(classFQName),
holder.myMethodEnumerator.enumerate(methodName)
)
@@ -138,23 +195,74 @@ public class TestDiscoveryIndex implements ProjectComponent {
}
}
return result;
} catch (Throwable throwable) {
thingsWentWrongLetsReinitialize(holder, throwable);
return Collections.emptyList();
}
}
});
}
private Holder getHolder() {
Holder holder = myHolder;
static class TestDataController {
private final Object myLock = new Object();
private String myBasePath;
private final boolean myReadOnly;
private volatile TestInfoHolder myHolder;
if (holder == null) {
synchronized (ourLock) {
holder = myHolder;
if (holder == null) holder = myHolder = new Holder();
TestDataController(String basePath, boolean readonly) {
myReadOnly = readonly;
init(basePath);
}
void init(String basePath) {
if (myHolder != null) dispose();
synchronized (myLock) {
myBasePath = basePath;
}
}
private TestInfoHolder getHolder() {
TestInfoHolder holder = myHolder;
if (holder == null) {
synchronized (myLock) {
holder = myHolder;
if (holder == null && myBasePath != null) holder = myHolder = new TestInfoHolder(myBasePath, myReadOnly, myLock);
}
}
return holder;
}
private void dispose() {
synchronized (myLock) {
TestInfoHolder holder = myHolder;
if (holder != null) {
holder.dispose();
myHolder = null;
}
}
}
private void thingsWentWrongLetsReinitialize(@Nullable TestInfoHolder holder, Throwable throwable) throws IOException {
LOG.error("Unexpected problem", throwable);
if (holder != null) holder.dispose();
final File versionFile = TestInfoHolder.getVersionFile(myBasePath);
FileUtil.delete(versionFile);
myHolder = null;
if (throwable instanceof IOException) throw (IOException) throwable;
}
public <R> R withTestDataHolder(ThrowableConvertor<TestInfoHolder, R, IOException> action) throws IOException {
synchronized (myLock) {
TestInfoHolder holder = getHolder();
if (holder == null || holder.isDisposed()) return null;
try {
return action.convert(holder);
} catch (Throwable throwable) {
if (!myReadOnly) thingsWentWrongLetsReinitialize(holder, throwable);
else LOG.error(throwable);
}
return null;
}
}
return holder;
}
public static TestDiscoveryIndex getInstance(Project project) {
@@ -167,13 +275,8 @@ public class TestDiscoveryIndex implements ProjectComponent {
@Override
public void disposeComponent() {
synchronized (ourLock) {
Holder holder = myHolder;
if (holder != null) {
holder.dispose();
myHolder = null;
}
}
myLocalTestRunDataController.dispose();
myRemoteTestRunDataController.dispose();
}
@NotNull
@@ -190,285 +293,6 @@ public class TestDiscoveryIndex implements ProjectComponent {
public void projectClosed() {
}
private static final int VERSION = 4;
private final class Holder {
final PersistentHashMap<Long, TIntArrayList> myMethodQNameToTestNames;
final PersistentHashMap<Integer, TIntObjectHashMap<TIntArrayList>> myTestNameToUsedClassesAndMethodMap;
final PersistentHashMap<Long, TIntArrayList> myTestNameToNearestModule;
final PersistentStringEnumerator myClassEnumerator;
final CachingEnumerator<String> myClassEnumeratorCache;
final PersistentStringEnumerator myMethodEnumerator;
final CachingEnumerator<String> myMethodEnumeratorCache;
final PersistentStringEnumerator myTestNameEnumerator;
final PersistentStringEnumerator myModuleNameEnumerator;
final List<PersistentEnumeratorDelegate> myConstructedDataFiles = new ArrayList<PersistentEnumeratorDelegate>(6);
private ScheduledFuture<?> myFlushingFuture;
private boolean myDisposed;
Holder() {
final File versionFile = getVersionFile(myBasePath);
versionFile.getParentFile().mkdirs();
final File methodQNameToTestNameFile = new File(myBasePath + File.separator + "methodQNameToTestName.data");
final File testNameToUsedClassesAndMethodMapFile = new File(myBasePath + File.separator + "testToCalledMethodNames.data");
final File classNameEnumeratorFile = new File(myBasePath + File.separator + "classNameEnumerator.data");
final File methodNameEnumeratorFile = new File(myBasePath + File.separator + "methodNameEnumerator.data");
final File testNameEnumeratorFile = new File(myBasePath + File.separator + "testNameEnumerator.data");
final File moduleNameEnumeratorFile = new File(myBasePath + File.separator + "moduleNameEnumerator.data");
final File testNameToNearestModuleFile = new File(myBasePath + File.separator + "testNameToNearestModule.data");
try {
int version = readVersion(versionFile);
if (version != VERSION) {
LOG.info("TestDiscoveryIndex was rewritten due to version change");
deleteAllIndexDataFiles(methodQNameToTestNameFile,
testNameToUsedClassesAndMethodMapFile,
classNameEnumeratorFile,
methodNameEnumeratorFile,
testNameEnumeratorFile, moduleNameEnumeratorFile,
testNameToNearestModuleFile);
writeVersion(versionFile);
}
PersistentHashMap<Long, TIntArrayList> methodQNameToTestNames;
PersistentHashMap<Integer, TIntObjectHashMap<TIntArrayList>> testNameToUsedClassesAndMethodMap;
PersistentHashMap<Long, TIntArrayList> testNameToNearestModule;
PersistentStringEnumerator classNameEnumerator;
PersistentStringEnumerator methodNameEnumerator;
PersistentStringEnumerator testNameEnumerator;
PersistentStringEnumerator moduleNameEnumerator;
int iterations = 0;
while(true) {
++iterations;
try {
methodQNameToTestNames = new PersistentHashMap<Long, TIntArrayList>(
methodQNameToTestNameFile,
new MethodQNameSerializer(),
new TestNamesExternalizer()
);
myConstructedDataFiles.add(methodQNameToTestNames);
testNameToUsedClassesAndMethodMap = new PersistentHashMap<Integer, TIntObjectHashMap<TIntArrayList>>(
testNameToUsedClassesAndMethodMapFile,
EnumeratorIntegerDescriptor.INSTANCE,
new ClassesAndMethodsMapDataExternalizer()
);
myConstructedDataFiles.add(testNameToUsedClassesAndMethodMap);
testNameToNearestModule = new PersistentHashMap<Long, TIntArrayList>(testNameToNearestModuleFile,
new MethodQNameSerializer(),
new TestNamesExternalizer());
myConstructedDataFiles.add(testNameToNearestModule);
classNameEnumerator = new PersistentStringEnumerator(classNameEnumeratorFile);
myConstructedDataFiles.add(classNameEnumerator);
methodNameEnumerator = new PersistentStringEnumerator(methodNameEnumeratorFile);
myConstructedDataFiles.add(methodNameEnumerator);
moduleNameEnumerator = new PersistentStringEnumerator(moduleNameEnumeratorFile);
myConstructedDataFiles.add(moduleNameEnumerator);
testNameEnumerator = new PersistentStringEnumerator(testNameEnumeratorFile);
myConstructedDataFiles.add(testNameEnumerator);
break;
} catch (Throwable throwable) {
LOG.info("TestDiscoveryIndex problem", throwable);
closeAllConstructedFiles(true);
myConstructedDataFiles.clear();
deleteAllIndexDataFiles(methodQNameToTestNameFile, testNameToUsedClassesAndMethodMapFile, classNameEnumeratorFile, methodNameEnumeratorFile,
testNameEnumeratorFile, moduleNameEnumeratorFile, testNameToNearestModuleFile);
// try another time
}
if (iterations >= 3) {
LOG.error("Unexpected circular initialization problem");
assert false;
}
}
myMethodQNameToTestNames = methodQNameToTestNames;
myTestNameToUsedClassesAndMethodMap = testNameToUsedClassesAndMethodMap;
myTestNameToNearestModule = testNameToNearestModule;
myClassEnumerator = classNameEnumerator;
myMethodEnumerator = methodNameEnumerator;
myTestNameEnumerator = testNameEnumerator;
myModuleNameEnumerator = moduleNameEnumerator;
myMethodEnumeratorCache = new CachingEnumerator<String>(methodNameEnumerator, EnumeratorStringDescriptor.INSTANCE);
myClassEnumeratorCache = new CachingEnumerator<String>(classNameEnumerator, EnumeratorStringDescriptor.INSTANCE);
myFlushingFuture = FlushingDaemon.everyFiveSeconds(() -> {
synchronized (ourLock) {
if (myDisposed) {
myFlushingFuture.cancel(false);
return;
}
for(PersistentEnumeratorDelegate dataFile:myConstructedDataFiles) {
if (dataFile.isDirty()) {
dataFile.force();
}
}
myClassEnumeratorCache.clear();
myMethodEnumeratorCache.clear();
}
});
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private void closeAllConstructedFiles(boolean ignoreCloseProblem) {
for(Closeable closeable:myConstructedDataFiles) {
try {
closeable.close();
} catch (Throwable throwable) {
if (!ignoreCloseProblem) throw new RuntimeException(throwable);
}
}
}
private void deleteAllIndexDataFiles(File... files) {
for (File file : files) {
IOUtil.deleteAllFilesStartingWith(file);
}
}
private void writeVersion(File versionFile) throws IOException {
final DataOutputStream versionOut = new DataOutputStream(new FileOutputStream(versionFile));
try {
DataInputOutputUtil.writeINT(versionOut, VERSION);
} finally {
try { versionOut.close(); } catch (IOException ignore) {}
}
}
private int readVersion(File versionFile) throws IOException {
if (!versionFile.exists()) return 0;
final DataInputStream versionInput = new DataInputStream(new FileInputStream(versionFile));
int version;
try {
version = DataInputOutputUtil.readINT(versionInput);
} finally {
try { versionInput.close(); } catch (IOException ignore) {}
}
return version;
}
void dispose() {
assert Thread.holdsLock(ourLock);
try {
closeAllConstructedFiles(false);
}
finally {
myDisposed = true;
}
}
private class TestNamesExternalizer implements DataExternalizer<TIntArrayList> {
public void save(@NotNull DataOutput dataOutput, TIntArrayList testNameIds) throws IOException {
for (int testNameId : testNameIds.toNativeArray()) DataInputOutputUtil.writeINT(dataOutput, testNameId);
}
public TIntArrayList read(@NotNull DataInput dataInput) throws IOException {
TIntHashSet result = new TIntHashSet();
while (((InputStream)dataInput).available() > 0) {
int id = DataInputOutputUtil.readINT(dataInput);
if (REMOVED_MARKER == id) {
id = DataInputOutputUtil.readINT(dataInput);
result.remove(id);
}
else {
result.add(id);
}
}
return new TIntArrayList(result.toArray());
}
}
private class ClassesAndMethodsMapDataExternalizer implements DataExternalizer<TIntObjectHashMap<TIntArrayList>> {
public void save(@NotNull final DataOutput dataOutput, TIntObjectHashMap<TIntArrayList> classAndMethodsMap)
throws IOException {
DataInputOutputUtil.writeINT(dataOutput, classAndMethodsMap.size());
final int[] classNameIds = classAndMethodsMap.keys();
Arrays.sort(classNameIds);
int prevClassNameId = 0;
for(int classNameId:classNameIds) {
DataInputOutputUtil.writeINT(dataOutput, classNameId - prevClassNameId);
TIntArrayList value = classAndMethodsMap.get(classNameId);
DataInputOutputUtil.writeINT(dataOutput, value.size());
final int[] methodNameIds = value.toNativeArray();
Arrays.sort(methodNameIds);
int prevMethodNameId = 0;
for (int methodNameId : methodNameIds) {
DataInputOutputUtil.writeINT(dataOutput, methodNameId - prevMethodNameId);
prevMethodNameId = methodNameId;
}
prevClassNameId = classNameId;
}
}
public TIntObjectHashMap<TIntArrayList> read(@NotNull DataInput dataInput) throws IOException {
int numberOfClasses = DataInputOutputUtil.readINT(dataInput);
TIntObjectHashMap<TIntArrayList> result = new TIntObjectHashMap<TIntArrayList>();
int prevClassNameId = 0;
while (numberOfClasses-- > 0) {
int classNameId = DataInputOutputUtil.readINT(dataInput) + prevClassNameId;
int numberOfMethods = DataInputOutputUtil.readINT(dataInput);
TIntArrayList methodNameIds = new TIntArrayList(numberOfMethods);
int prevMethodNameId = 0;
while (numberOfMethods-- > 0) {
final int methodNameId = DataInputOutputUtil.readINT(dataInput) + prevMethodNameId;
methodNameIds.add(methodNameId);
prevMethodNameId = methodNameId;
}
result.put(classNameId, methodNameIds);
prevClassNameId = classNameId;
}
return result;
}
}
}
private static class MethodQNameSerializer implements KeyDescriptor<Long> {
public static final MethodQNameSerializer INSTANCE = new MethodQNameSerializer();
@Override
public void save(@NotNull DataOutput out, Long value) throws IOException {
out.writeLong(value);
}
@Override
public Long read(@NotNull DataInput in) throws IOException {
return in.readLong();
}
@Override
public int getHashCode(Long value) {
return value.hashCode();
}
@Override
public boolean isEqual(Long val1, Long val2) {
return val1.equals(val2);
}
}
public void updateFromTestTrace(@NotNull File file, @Nullable JavaTestConfigurationBase configurationBase) throws IOException {
int fileNameDotIndex = file.getName().lastIndexOf('.');
final String testName = fileNameDotIndex != -1 ? file.getName().substring(0, fileNameDotIndex) : file.getName();
@@ -477,158 +301,48 @@ public class TestDiscoveryIndex implements ProjectComponent {
}
private void doUpdateFromTestTrace(File file, final String testName, @Nullable final String moduleName) throws IOException {
synchronized (ourLock) {
Holder holder = getHolder();
if (holder.myDisposed) return;
try {
final int testNameId = holder.myTestNameEnumerator.enumerate(testName);
TIntObjectHashMap<TIntArrayList> classData = loadClassAndMethodsMap(file, holder);
TIntObjectHashMap<TIntArrayList> previousClassData = holder.myTestNameToUsedClassesAndMethodMap.get(testNameId);
doUpdateFromDiff(holder, testNameId, classData, previousClassData, moduleName != null ? holder.myModuleNameEnumerator.enumerate(moduleName) : null);
} catch (Throwable throwable) {
thingsWentWrongLetsReinitialize(holder, throwable);
}
}
}
private void doUpdateFromDiff(Holder holder,
final int testNameId,
@Nullable TIntObjectHashMap<TIntArrayList> classData,
@Nullable TIntObjectHashMap<TIntArrayList> previousClassData,
@Nullable Integer moduleId) throws IOException {
ValueDiff valueDiff = new ValueDiff(classData, previousClassData);
if (valueDiff.hasRemovedDelta()) {
for (int classQName : valueDiff.myRemovedClassData.keys()) {
for (int methodName : valueDiff.myRemovedClassData.get(classQName).toNativeArray()) {
holder.myMethodQNameToTestNames.appendData(createKey(classQName, methodName),
new PersistentHashMap.ValueDataAppender() {
@Override
public void append(DataOutput dataOutput) throws IOException {
DataInputOutputUtil.writeINT(dataOutput, REMOVED_MARKER);
DataInputOutputUtil.writeINT(dataOutput, testNameId);
}
}
);
myLocalTestRunDataController.withTestDataHolder(new ThrowableConvertor<TestInfoHolder, Void, IOException>() {
@Override
public Void convert(TestInfoHolder localHolder) throws IOException {
final int testNameId = localHolder.myTestNameEnumerator.enumerate(testName);
TIntObjectHashMap<TIntArrayList> classData = loadClassAndMethodsMap(file, localHolder);
TIntObjectHashMap<TIntArrayList> previousClassData = localHolder.myTestNameToUsedClassesAndMethodMap.get(testNameId);
if (previousClassData == null) {
previousClassData = myRemoteTestRunDataController.withTestDataHolder(
remoteDataHolder -> {
TIntObjectHashMap<TIntArrayList> remoteClassData = remoteDataHolder.myTestNameToUsedClassesAndMethodMap.get(testNameId);
if (remoteClassData == null) return null;
TIntObjectHashMap<TIntArrayList> result = new TIntObjectHashMap<TIntArrayList>(remoteClassData.size());
Ref<IOException> exceptionRef = new Ref<IOException>();
boolean processingResult = remoteClassData.forEachEntry((remoteClassKey, remoteClassMethodIds) -> {
try {
int localClassKey =
localHolder.myClassEnumeratorCache.enumerate(remoteDataHolder.myClassEnumeratorCache.valueOf(remoteClassKey));
TIntArrayList localClassIds = new TIntArrayList(remoteClassMethodIds.size());
for (int methodId : remoteClassMethodIds.toNativeArray()) {
localClassIds
.add(localHolder.myMethodEnumeratorCache.enumerate(remoteDataHolder.myMethodEnumeratorCache.valueOf(methodId)));
}
result.put(localClassKey, localClassIds);
return true;
} catch (IOException ex) {
exceptionRef.set(ex);
return false;
}
});
if (!processingResult) throw exceptionRef.get();
return result;
});
}
}
}
if (valueDiff.hasAddedDelta()) {
for (int classQName : valueDiff.myAddedOrChangedClassData.keys()) {
for (int methodName : valueDiff.myAddedOrChangedClassData.get(classQName).toNativeArray()) {
holder.myMethodQNameToTestNames.appendData(createKey(classQName, methodName),
new PersistentHashMap.ValueDataAppender() {
@Override
public void append(DataOutput dataOutput) throws IOException {
DataInputOutputUtil.writeINT(dataOutput, testNameId);
}
});
if (moduleId != null) {
holder.myTestNameToNearestModule.appendData(createKey(classQName, methodName),
new PersistentHashMap.ValueDataAppender() {
@Override
public void append(DataOutput dataOutput) throws IOException {
DataInputOutputUtil.writeINT(dataOutput, moduleId);
}
});
}
}
localHolder.doUpdateFromDiff(testNameId, classData, previousClassData, moduleName != null ? localHolder.myModuleNameEnumerator.enumerate(moduleName) : null);
return null;
}
}
if ((valueDiff.hasAddedDelta() || valueDiff.hasRemovedDelta())) {
if(classData != null) {
holder.myTestNameToUsedClassesAndMethodMap.put(testNameId, classData);
} else {
holder.myTestNameToUsedClassesAndMethodMap.remove(testNameId);
}
}
});
}
@NotNull
private static File getVersionFile(String path) {
return new File(path + File.separator + "index.version");
}
private void thingsWentWrongLetsReinitialize(@Nullable Holder holder, Throwable throwable) throws IOException {
LOG.error("Unexpected problem", throwable);
if (holder != null) holder.dispose();
String path = TestDiscoveryExtension.baseTestDiscoveryPathForProject(myProject);
final File versionFile = getVersionFile(path);
FileUtil.delete(versionFile);
myHolder = null;
if (throwable instanceof IOException) throw (IOException) throwable;
}
private static long createKey(int classQName, int methodName) {
return ((long)classQName << 32) | methodName;
}
static class ValueDiff {
final TIntObjectHashMap<TIntArrayList> myAddedOrChangedClassData;
final TIntObjectHashMap<TIntArrayList> myRemovedClassData;
ValueDiff(@Nullable TIntObjectHashMap<TIntArrayList> classData, @Nullable TIntObjectHashMap<TIntArrayList> previousClassData) {
TIntObjectHashMap<TIntArrayList> addedOrChangedClassData = classData;
TIntObjectHashMap<TIntArrayList> removedClassData = previousClassData;
if (previousClassData != null && !previousClassData.isEmpty()) {
removedClassData = new TIntObjectHashMap<TIntArrayList>();
addedOrChangedClassData = new TIntObjectHashMap<TIntArrayList>();
if (classData != null) {
for (int classQName : classData.keys()) {
TIntArrayList currentMethods = classData.get(classQName);
TIntArrayList previousMethods = previousClassData.get(classQName);
if (previousMethods == null) {
addedOrChangedClassData.put(classQName, currentMethods);
continue;
}
final int[] previousMethodIds = previousMethods.toNativeArray();
TIntHashSet previousMethodsSet = new TIntHashSet(previousMethodIds);
final int[] currentMethodIds = currentMethods.toNativeArray();
TIntHashSet currentMethodsSet = new TIntHashSet(currentMethodIds);
currentMethodsSet.removeAll(previousMethodIds);
previousMethodsSet.removeAll(currentMethodIds);
if (!currentMethodsSet.isEmpty()) {
addedOrChangedClassData.put(classQName, new TIntArrayList(currentMethodsSet.toArray()));
}
if (!previousMethodsSet.isEmpty()) {
removedClassData.put(classQName, new TIntArrayList(previousMethodsSet.toArray()));
}
}
}
if (classData != null) {
for (int classQName : previousClassData.keys()) {
if (classData.containsKey(classQName)) continue;
TIntArrayList previousMethods = previousClassData.get(classQName);
removedClassData.put(classQName, previousMethods);
}
}
}
myAddedOrChangedClassData = addedOrChangedClassData;
myRemovedClassData = removedClassData;
}
public boolean hasRemovedDelta() {
return myRemovedClassData != null && !myRemovedClassData.isEmpty();
}
public boolean hasAddedDelta() {
return myAddedOrChangedClassData != null && !myAddedOrChangedClassData.isEmpty();
}
}
@NotNull
private static TIntObjectHashMap<TIntArrayList> loadClassAndMethodsMap(File file, Holder holder) throws IOException {
private static TIntObjectHashMap<TIntArrayList> loadClassAndMethodsMap(File file, TestInfoHolder holder) throws IOException {
DataInputStream inputStream = new DataInputStream(new BufferedInputStream(new FileInputStream(file), 64 * 1024));
byte[] buffer = IOUtil.allocReadWriteUTFBuffer();
@@ -0,0 +1,453 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* 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.intellij.execution.testDiscovery;
import com.intellij.openapi.vfs.newvfs.persistent.FlushingDaemon;
import com.intellij.util.io.*;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntHashSet;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
final class TestInfoHolder {
final PersistentHashMap<Long, TIntArrayList> myMethodQNameToTestNames;
final PersistentHashMap<Integer, TIntObjectHashMap<TIntArrayList>> myTestNameToUsedClassesAndMethodMap;
final PersistentHashMap<Long, TIntArrayList> myTestNameToNearestModule;
final PersistentStringEnumerator myClassEnumerator;
final CachingEnumerator<String> myClassEnumeratorCache;
final PersistentStringEnumerator myMethodEnumerator;
final CachingEnumerator<String> myMethodEnumeratorCache;
final PersistentStringEnumerator myTestNameEnumerator;
final PersistentStringEnumerator myModuleNameEnumerator;
final List<PersistentEnumeratorDelegate> myConstructedDataFiles = new ArrayList<PersistentEnumeratorDelegate>(6);
private ScheduledFuture<?> myFlushingFuture;
private boolean myDisposed;
private final Object myLock;
private static final int VERSION = 4;
TestInfoHolder(String basePath, boolean readOnly, Object lock) {
myLock = lock;
final File versionFile = getVersionFile(basePath);
versionFile.getParentFile().mkdirs();
final File methodQNameToTestNameFile = new File(basePath + File.separator + "methodQNameToTestName.data");
final File testNameToUsedClassesAndMethodMapFile = new File(basePath + File.separator + "testToCalledMethodNames.data");
final File classNameEnumeratorFile = new File(basePath + File.separator + "classNameEnumerator.data");
final File methodNameEnumeratorFile = new File(basePath + File.separator + "methodNameEnumerator.data");
final File testNameEnumeratorFile = new File(basePath + File.separator + "testNameEnumerator.data");
final File moduleNameEnumeratorFile = new File(basePath + File.separator + "moduleNameEnumerator.data");
final File testNameToNearestModuleFile = new File(basePath + File.separator + "testNameToNearestModule.data");
try {
int version = readVersion(versionFile);
if (version != VERSION) {
TestDiscoveryIndex.LOG.info("TestDiscoveryIndex was rewritten due to version change");
deleteAllIndexDataFiles(methodQNameToTestNameFile,
testNameToUsedClassesAndMethodMapFile,
classNameEnumeratorFile,
methodNameEnumeratorFile,
testNameEnumeratorFile, moduleNameEnumeratorFile,
testNameToNearestModuleFile);
writeVersion(versionFile);
}
PersistentHashMap<Long, TIntArrayList> methodQNameToTestNames;
PersistentHashMap<Integer, TIntObjectHashMap<TIntArrayList>> testNameToUsedClassesAndMethodMap;
PersistentHashMap<Long, TIntArrayList> testNameToNearestModule;
PersistentStringEnumerator classNameEnumerator;
PersistentStringEnumerator methodNameEnumerator;
PersistentStringEnumerator testNameEnumerator;
PersistentStringEnumerator moduleNameEnumerator;
int iterations = 0;
while (true) {
++iterations;
try {
methodQNameToTestNames = new PersistentHashMap<Long, TIntArrayList>(
methodQNameToTestNameFile,
MethodQNameSerializer.INSTANCE,
new TestNamesExternalizer()
) {
@Override
protected boolean isReadOnly() {
return readOnly;
}
};
myConstructedDataFiles.add(methodQNameToTestNames);
testNameToUsedClassesAndMethodMap = new PersistentHashMap<Integer, TIntObjectHashMap<TIntArrayList>>(
testNameToUsedClassesAndMethodMapFile,
EnumeratorIntegerDescriptor.INSTANCE,
new ClassesAndMethodsMapDataExternalizer()
) {
@Override
protected boolean isReadOnly() {
return readOnly;
}
};
myConstructedDataFiles.add(testNameToUsedClassesAndMethodMap);
testNameToNearestModule = new PersistentHashMap<Long, TIntArrayList>(testNameToNearestModuleFile,
MethodQNameSerializer.INSTANCE,
new TestNamesExternalizer());
myConstructedDataFiles.add(testNameToNearestModule);
classNameEnumerator = new PersistentStringEnumerator(classNameEnumeratorFile);
myConstructedDataFiles.add(classNameEnumerator);
methodNameEnumerator = new PersistentStringEnumerator(methodNameEnumeratorFile);
myConstructedDataFiles.add(methodNameEnumerator);
moduleNameEnumerator = new PersistentStringEnumerator(moduleNameEnumeratorFile);
myConstructedDataFiles.add(moduleNameEnumerator);
testNameEnumerator = new PersistentStringEnumerator(testNameEnumeratorFile);
myConstructedDataFiles.add(testNameEnumerator);
break;
}
catch (Throwable throwable) {
TestDiscoveryIndex.LOG.info("TestDiscoveryIndex problem", throwable);
closeAllConstructedFiles(true);
myConstructedDataFiles.clear();
deleteAllIndexDataFiles(methodQNameToTestNameFile, testNameToUsedClassesAndMethodMapFile, classNameEnumeratorFile,
methodNameEnumeratorFile,
testNameEnumeratorFile, moduleNameEnumeratorFile, testNameToNearestModuleFile);
// try another time
}
if (iterations >= 3) {
TestDiscoveryIndex.LOG.error("Unexpected circular initialization problem");
assert false;
}
}
myMethodQNameToTestNames = methodQNameToTestNames;
myTestNameToUsedClassesAndMethodMap = testNameToUsedClassesAndMethodMap;
myTestNameToNearestModule = testNameToNearestModule;
myClassEnumerator = classNameEnumerator;
myMethodEnumerator = methodNameEnumerator;
myTestNameEnumerator = testNameEnumerator;
myModuleNameEnumerator = moduleNameEnumerator;
myMethodEnumeratorCache = new CachingEnumerator<String>(methodNameEnumerator, EnumeratorStringDescriptor.INSTANCE);
myClassEnumeratorCache = new CachingEnumerator<String>(classNameEnumerator, EnumeratorStringDescriptor.INSTANCE);
myFlushingFuture = FlushingDaemon.everyFiveSeconds(() -> {
synchronized (myLock) {
if (myDisposed) {
myFlushingFuture.cancel(false);
return;
}
for (PersistentEnumeratorDelegate dataFile : myConstructedDataFiles) {
if (dataFile.isDirty()) {
dataFile.force();
}
}
myClassEnumeratorCache.clear();
myMethodEnumeratorCache.clear();
}
});
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private void closeAllConstructedFiles(boolean ignoreCloseProblem) {
for (Closeable closeable : myConstructedDataFiles) {
try {
closeable.close();
}
catch (Throwable throwable) {
if (!ignoreCloseProblem) throw new RuntimeException(throwable);
}
}
}
private static void deleteAllIndexDataFiles(File... files) {
for (File file : files) {
IOUtil.deleteAllFilesStartingWith(file);
}
}
private static void writeVersion(File versionFile) throws IOException {
final java.io.DataOutputStream versionOut = new java.io.DataOutputStream(new FileOutputStream(versionFile));
try {
DataInputOutputUtil.writeINT(versionOut, VERSION);
}
finally {
try {
versionOut.close();
}
catch (IOException ignore) {
}
}
}
private static int readVersion(File versionFile) throws IOException {
if (!versionFile.exists()) return 0;
final DataInputStream versionInput = new DataInputStream(new FileInputStream(versionFile));
int version;
try {
version = DataInputOutputUtil.readINT(versionInput);
}
finally {
try {
versionInput.close();
}
catch (IOException ignore) {
}
}
return version;
}
void dispose() {
assert Thread.holdsLock(myLock);
try {
closeAllConstructedFiles(false);
}
finally {
myDisposed = true;
}
}
private static final int REMOVED_MARKER = -1;
void doUpdateFromDiff(final int testNameId,
@Nullable TIntObjectHashMap<TIntArrayList> classData,
@Nullable TIntObjectHashMap<TIntArrayList> previousClassData,
@Nullable Integer moduleId) throws IOException {
ValueDiff valueDiff = new ValueDiff(classData, previousClassData);
if (valueDiff.hasRemovedDelta()) {
for (int classQName : valueDiff.myRemovedClassData.keys()) {
for (int methodName : valueDiff.myRemovedClassData.get(classQName).toNativeArray()) {
myMethodQNameToTestNames.appendData(createKey(classQName, methodName),
dataOutput -> {
DataInputOutputUtil.writeINT(dataOutput, REMOVED_MARKER);
DataInputOutputUtil.writeINT(dataOutput, testNameId);
}
);
}
}
}
if (valueDiff.hasAddedDelta()) {
for (int classQName : valueDiff.myAddedOrChangedClassData.keys()) {
for (int methodName : valueDiff.myAddedOrChangedClassData.get(classQName).toNativeArray()) {
myMethodQNameToTestNames.appendData(createKey(classQName, methodName),
dataOutput -> DataInputOutputUtil.writeINT(dataOutput, testNameId));
if (moduleId != null) {
myTestNameToNearestModule.appendData(createKey(classQName, methodName),
dataOutput -> DataInputOutputUtil.writeINT(dataOutput, moduleId));
}
}
}
}
if ((valueDiff.hasAddedDelta() || valueDiff.hasRemovedDelta())) {
if (classData != null) {
myTestNameToUsedClassesAndMethodMap.put(testNameId, classData);
}
else {
myTestNameToUsedClassesAndMethodMap.remove(testNameId);
}
}
}
public boolean isDisposed() {
return myDisposed;
}
private static class TestNamesExternalizer implements DataExternalizer<TIntArrayList> {
public void save(@NotNull DataOutput dataOutput, TIntArrayList testNameIds) throws IOException {
for (int testNameId : testNameIds.toNativeArray()) DataInputOutputUtil.writeINT(dataOutput, testNameId);
}
public TIntArrayList read(@NotNull DataInput dataInput) throws IOException {
TIntHashSet result = new TIntHashSet();
while (((InputStream)dataInput).available() > 0) {
int id = DataInputOutputUtil.readINT(dataInput);
if (REMOVED_MARKER == id) {
id = DataInputOutputUtil.readINT(dataInput);
if(!result.remove(id)) {
result.add(-id);
}
}
else {
result.add(id);
}
}
return new TIntArrayList(result.toArray());
}
}
private static class ClassesAndMethodsMapDataExternalizer implements DataExternalizer<TIntObjectHashMap<TIntArrayList>> {
public void save(@NotNull final DataOutput dataOutput, TIntObjectHashMap<TIntArrayList> classAndMethodsMap)
throws IOException {
DataInputOutputUtil.writeINT(dataOutput, classAndMethodsMap.size());
final int[] classNameIds = classAndMethodsMap.keys();
Arrays.sort(classNameIds);
int prevClassNameId = 0;
for (int classNameId : classNameIds) {
DataInputOutputUtil.writeINT(dataOutput, classNameId - prevClassNameId);
TIntArrayList value = classAndMethodsMap.get(classNameId);
DataInputOutputUtil.writeINT(dataOutput, value.size());
final int[] methodNameIds = value.toNativeArray();
Arrays.sort(methodNameIds);
int prevMethodNameId = 0;
for (int methodNameId : methodNameIds) {
DataInputOutputUtil.writeINT(dataOutput, methodNameId - prevMethodNameId);
prevMethodNameId = methodNameId;
}
prevClassNameId = classNameId;
}
}
public TIntObjectHashMap<TIntArrayList> read(@NotNull DataInput dataInput) throws IOException {
int numberOfClasses = DataInputOutputUtil.readINT(dataInput);
TIntObjectHashMap<TIntArrayList> result = new TIntObjectHashMap<TIntArrayList>();
int prevClassNameId = 0;
while (numberOfClasses-- > 0) {
int classNameId = DataInputOutputUtil.readINT(dataInput) + prevClassNameId;
int numberOfMethods = DataInputOutputUtil.readINT(dataInput);
TIntArrayList methodNameIds = new TIntArrayList(numberOfMethods);
int prevMethodNameId = 0;
while (numberOfMethods-- > 0) {
final int methodNameId = DataInputOutputUtil.readINT(dataInput) + prevMethodNameId;
methodNameIds.add(methodNameId);
prevMethodNameId = methodNameId;
}
result.put(classNameId, methodNameIds);
prevClassNameId = classNameId;
}
return result;
}
}
private static class MethodQNameSerializer implements KeyDescriptor<Long> {
public static final MethodQNameSerializer INSTANCE = new MethodQNameSerializer();
@Override
public void save(@NotNull DataOutput out, Long value) throws IOException {
out.writeLong(value);
}
@Override
public Long read(@NotNull DataInput in) throws IOException {
return in.readLong();
}
@Override
public int getHashCode(Long value) {
return value.hashCode();
}
@Override
public boolean isEqual(Long val1, Long val2) {
return val1.equals(val2);
}
}
@NotNull
static File getVersionFile(String path) {
return new File(path + File.separator + "index.version");
}
static long createKey(int classQName, int methodName) {
return ((long)classQName << 32) | methodName;
}
static class ValueDiff {
final TIntObjectHashMap<TIntArrayList> myAddedOrChangedClassData;
final TIntObjectHashMap<TIntArrayList> myRemovedClassData;
ValueDiff(@Nullable TIntObjectHashMap<TIntArrayList> classData, @Nullable TIntObjectHashMap<TIntArrayList> previousClassData) {
TIntObjectHashMap<TIntArrayList> addedOrChangedClassData = classData;
TIntObjectHashMap<TIntArrayList> removedClassData = previousClassData;
if (previousClassData != null && !previousClassData.isEmpty()) {
removedClassData = new TIntObjectHashMap<TIntArrayList>();
addedOrChangedClassData = new TIntObjectHashMap<TIntArrayList>();
if (classData != null) {
for (int classQName : classData.keys()) {
TIntArrayList currentMethods = classData.get(classQName);
TIntArrayList previousMethods = previousClassData.get(classQName);
if (previousMethods == null) {
addedOrChangedClassData.put(classQName, currentMethods);
continue;
}
final int[] previousMethodIds = previousMethods.toNativeArray();
TIntHashSet previousMethodsSet = new TIntHashSet(previousMethodIds);
final int[] currentMethodIds = currentMethods.toNativeArray();
TIntHashSet currentMethodsSet = new TIntHashSet(currentMethodIds);
currentMethodsSet.removeAll(previousMethodIds);
previousMethodsSet.removeAll(currentMethodIds);
if (!currentMethodsSet.isEmpty()) {
addedOrChangedClassData.put(classQName, new TIntArrayList(currentMethodsSet.toArray()));
}
if (!previousMethodsSet.isEmpty()) {
removedClassData.put(classQName, new TIntArrayList(previousMethodsSet.toArray()));
}
}
}
if (classData != null) {
for (int classQName : previousClassData.keys()) {
if (classData.containsKey(classQName)) continue;
TIntArrayList previousMethods = previousClassData.get(classQName);
removedClassData.put(classQName, previousMethods);
}
}
}
myAddedOrChangedClassData = addedOrChangedClassData;
myRemovedClassData = removedClassData;
}
public boolean hasRemovedDelta() {
return myRemovedClassData != null && !myRemovedClassData.isEmpty();
}
public boolean hasAddedDelta() {
return myAddedOrChangedClassData != null && !myAddedOrChangedClassData.isEmpty();
}
}
}