/*
* Copyright 2010-2015 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 org.jetbrains.kotlin.idea.debugger;
import com.google.common.collect.Lists;
import com.intellij.debugger.NoDataException;
import com.intellij.debugger.PositionManager;
import com.intellij.debugger.SourcePosition;
import com.intellij.debugger.engine.DebugProcess;
import com.intellij.debugger.engine.DebugProcessEvents;
import com.intellij.debugger.engine.DebugProcessImpl;
import com.intellij.debugger.jdi.VirtualMachineProxyImpl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.testFramework.LightProjectDescriptor;
import com.sun.jdi.Location;
import com.sun.jdi.ReferenceType;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.io.FilesKt;
import kotlin.jvm.functions.Function1;
import kotlin.sequences.SequencesKt;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.backend.common.output.OutputFileCollection;
import org.jetbrains.kotlin.codegen.ClassBuilderFactories;
import org.jetbrains.kotlin.codegen.GenerationUtils;
import org.jetbrains.kotlin.codegen.state.GenerationState;
import org.jetbrains.kotlin.config.CompilerConfiguration;
import org.jetbrains.kotlin.config.JVMConfigurationKeys;
import org.jetbrains.kotlin.descriptors.PackagePartProvider;
import org.jetbrains.kotlin.idea.debugger.evaluate.KotlinDebuggerCaches;
import org.jetbrains.kotlin.idea.project.PluginJetFilesProvider;
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase;
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor;
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.test.ConfigurationKind;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestJdkKind;
import org.jetbrains.kotlin.utils.ExceptionUtilsKt;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public abstract class AbstractPositionManagerTest extends KotlinLightCodeInsightFixtureTestCase {
// Breakpoint is given as a line comment on a specific line, containing the regexp to match the name of the class where that line
// can be found. This pattern matches against these line comments and saves the class name in the first group
private static final Pattern BREAKPOINT_PATTERN = Pattern.compile("^.*//\\s*(.+)\\s*$");
@NotNull
@Override
protected String getTestDataPath() {
return PluginTestCaseBase.getTestDataPathBase() + "/debugger/positionManager/";
}
@Override
public void setUp() {
super.setUp();
myFixture.setTestDataPath(PluginTestCaseBase.getTestDataPathBase());
}
private DebugProcessImpl debugProcess;
@NotNull
@Override
protected LightProjectDescriptor getProjectDescriptor() {
return KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE;
}
@NotNull
private static KotlinPositionManager createPositionManager(
@NotNull DebugProcess process,
@NotNull List<KtFile> files,
@NotNull GenerationState state
) {
KotlinPositionManager positionManager = (KotlinPositionManager) new KotlinPositionManagerFactory().createPositionManager(process);
assertNotNull(positionManager);
for (KtFile file : files) {
KotlinDebuggerCaches.Companion.addTypeMapper(file, state.getTypeMapper());
}
return positionManager;
}
protected void doTest(@NotNull String fileName) throws Exception {
if (fileName.endsWith(".kt")) {
String path = getPath(fileName);
myFixture.configureByFile(path);
}
else {
String path = getPath(fileName);
SequencesKt.forEach(FilesKt.walkTopDown(new File(path)), new Function1<File, Unit>() {
@Override
public Unit invoke(File file) {
String fileName = file.getName();
String path = getPath(fileName);
myFixture.configureByFile(path);
return null;
}
});
}
performTest();
}
@NotNull
private static String getPath(@NotNull String fileName) {
return StringsKt.substringAfter(fileName, PluginTestCaseBase.TEST_DATA_PROJECT_RELATIVE.substring(1), fileName);
}
private void performTest() {
Project project = getProject();
List<KtFile> files = new ArrayList<>(PluginJetFilesProvider.allFilesInProject(project));
if (files.isEmpty()) return;
List<Breakpoint> breakpoints = Lists.newArrayList();
for (KtFile file : files) {
breakpoints.addAll(extractBreakpointsInfo(file, file.getText()));
}
CompilerConfiguration configuration = KotlinTestUtils.newConfiguration(ConfigurationKind.JDK_ONLY, TestJdkKind.MOCK_JDK);
// TODO: delete this once IDEVirtualFileFinder supports loading .kotlin_builtins files
configuration.put(JVMConfigurationKeys.ADD_BUILT_INS_FROM_COMPILER_TO_DEPENDENCIES, true);
GenerationState state =
GenerationUtils.compileFiles(files, configuration, ClassBuilderFactories.TEST, scope -> PackagePartProvider.Empty.INSTANCE);
Map<String, ReferenceType> referencesByName = getReferenceMap(state.getFactory());
debugProcess = createDebugProcess(referencesByName);
PositionManager positionManager = createPositionManager(debugProcess, files, state);
ApplicationManager.getApplication().runReadAction(() -> {
try {
for (Breakpoint breakpoint : breakpoints) {
assertBreakpointIsHandledCorrectly(breakpoint, positionManager);
}
}
catch (NoDataException e) {
throw ExceptionUtilsKt.rethrow(e);
}
});
}
@Override
public void tearDown() {
if (debugProcess != null) {
debugProcess.stop(true);
debugProcess.dispose();
debugProcess = null;
}
super.tearDown();
}
private static Collection<Breakpoint> extractBreakpointsInfo(KtFile file, String fileContent) {
Collection<Breakpoint> breakpoints = Lists.newArrayList();
String[] lines = StringUtil.convertLineSeparators(fileContent).split("\n");
for (int i = 0; i < lines.length; i++) {
Matcher matcher = BREAKPOINT_PATTERN.matcher(lines[i]);
if (matcher.matches()) {
breakpoints.add(new Breakpoint(file, i, matcher.group(1)));
}
}
return breakpoints;
}
private static Map<String, ReferenceType> getReferenceMap(OutputFileCollection outputFiles) {
return new SmartMockReferenceTypeContext(outputFiles).getReferenceTypesByName();
}
private DebugProcessEvents createDebugProcess(Map<String, ReferenceType> referencesByName) {
return new DebugProcessEvents(getProject()) {
private VirtualMachineProxyImpl virtualMachineProxy;
@NotNull
@Override
public VirtualMachineProxyImpl getVirtualMachineProxy() {
if (virtualMachineProxy == null) {
virtualMachineProxy = new MockVirtualMachineProxy(this, referencesByName);
}
return virtualMachineProxy;
}
@NotNull
@Override
public GlobalSearchScope getSearchScope() {
return GlobalSearchScope.allScope(getProject());
}
};
}
private static void assertBreakpointIsHandledCorrectly(Breakpoint breakpoint, PositionManager positionManager) throws NoDataException {
SourcePosition position = SourcePosition.createFromLine(breakpoint.file, breakpoint.lineNumber);
List<ReferenceType> classes = positionManager.getAllClasses(position);
assertNotNull(classes);
assertFalse("Classes not found for line " + (breakpoint.lineNumber + 1) + ", expected " + breakpoint.classNameRegexp,
classes.isEmpty());
if (classes.stream().noneMatch(clazz -> clazz.name().matches(breakpoint.classNameRegexp))) {
throw new AssertionError("Breakpoint class '" + breakpoint.classNameRegexp +
"' from line " + (breakpoint.lineNumber + 1) + " was not found in the PositionManager classes names: " +
classes.stream().map(ReferenceType::name).collect(Collectors.joining(",")));
}
ReferenceType typeWithFqName = classes.get(0);
Location location = new MockLocation(typeWithFqName, breakpoint.file.getName(), breakpoint.lineNumber + 1);
SourcePosition actualPosition = positionManager.getSourcePosition(location);
assertNotNull(actualPosition);
assertEquals(position.getFile(), actualPosition.getFile());
assertEquals(position.getLine(), actualPosition.getLine());
}
private static class Breakpoint {
private final KtFile file;
private final int lineNumber; // 0-based
private final String classNameRegexp;
private Breakpoint(KtFile file, int lineNumber, String classNameRegexp) {
this.file = file;
this.lineNumber = lineNumber;
this.classNameRegexp = classNameRegexp;
}
}
private static class MockVirtualMachineProxy extends VirtualMachineProxyImpl {
private final Map<String, ReferenceType> referencesByName;
private MockVirtualMachineProxy(DebugProcessEvents debugProcess, Map<String, ReferenceType> referencesByName) {
super(debugProcess, new MockVirtualMachine());
this.referencesByName = referencesByName;
}
@Override
public List<ReferenceType> allClasses() {
return new ArrayList<>(referencesByName.values());
}
@Override
public List<ReferenceType> classesByName(String name) {
return CollectionsKt.listOfNotNull(referencesByName.get(name));
}
}
}