/* * 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.codegen; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtil; import com.intellij.testFramework.TestDataFile; import kotlin.collections.ArraysKt; import kotlin.io.FilesKt; import kotlin.text.Charsets; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.backend.common.output.OutputFile; import org.jetbrains.kotlin.checkers.CheckerTestUtil; import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys; import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles; import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment; import org.jetbrains.kotlin.cli.jvm.config.JvmContentRootsKt; import org.jetbrains.kotlin.codegen.forTestCompile.ForTestCompileRuntime; import org.jetbrains.kotlin.config.CompilerConfiguration; import org.jetbrains.kotlin.config.CompilerConfigurationKey; import org.jetbrains.kotlin.config.JVMConfigurationKeys; import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil; import org.jetbrains.kotlin.name.FqName; import org.jetbrains.kotlin.psi.KtFile; import org.jetbrains.kotlin.test.ConfigurationKind; import org.jetbrains.kotlin.test.InTextDirectivesUtils; import org.jetbrains.kotlin.test.KotlinTestUtils; import org.jetbrains.kotlin.test.TestJdkKind; import org.jetbrains.kotlin.test.testFramework.KtUsefulTestCase; import org.jetbrains.kotlin.utils.ExceptionUtilsKt; import org.jetbrains.org.objectweb.asm.ClassReader; import org.jetbrains.org.objectweb.asm.tree.ClassNode; import org.jetbrains.org.objectweb.asm.tree.MethodNode; import org.jetbrains.org.objectweb.asm.tree.analysis.Analyzer; import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException; import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue; import org.jetbrains.org.objectweb.asm.tree.analysis.SimpleVerifier; import org.jetbrains.org.objectweb.asm.util.Textifier; import org.jetbrains.org.objectweb.asm.util.TraceMethodVisitor; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.jetbrains.kotlin.codegen.CodegenTestUtil.*; import static org.jetbrains.kotlin.test.KotlinTestUtils.getAnnotationsJar; public abstract class CodegenTestCase extends KtUsefulTestCase { private static final String DEFAULT_TEST_FILE_NAME = "a_test"; protected KotlinCoreEnvironment myEnvironment; protected CodegenTestFiles myFiles; protected ClassFileFactory classFileFactory; protected GeneratedClassLoader initializedClassLoader; protected ConfigurationKind configurationKind = ConfigurationKind.JDK_ONLY; protected final void createEnvironmentWithMockJdkAndIdeaAnnotations( @NotNull ConfigurationKind configurationKind, @Nullable File... javaSourceRoots ) { createEnvironmentWithMockJdkAndIdeaAnnotations(configurationKind, Collections.emptyList(), javaSourceRoots); } @NotNull protected static TestJdkKind getJdkKind(@NotNull List files) { for (TestFile file : files) { if (InTextDirectivesUtils.isDirectiveDefined(file.content, "FULL_JDK")) { return TestJdkKind.FULL_JDK; } } return TestJdkKind.MOCK_JDK; } protected final void createEnvironmentWithMockJdkAndIdeaAnnotations( @NotNull ConfigurationKind configurationKind, @NotNull List testFilesWithConfigurationDirectives, @Nullable File... javaSourceRoots ) { if (myEnvironment != null) { throw new IllegalStateException("must not set up myEnvironment twice"); } CompilerConfiguration configuration = createConfiguration( configurationKind, TestJdkKind.MOCK_JDK, Collections.singletonList(getAnnotationsJar()), ArraysKt.filterNotNull(javaSourceRoots), testFilesWithConfigurationDirectives ); myEnvironment = KotlinCoreEnvironment.createForTests( getTestRootDisposable(), configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES ); } @NotNull protected static CompilerConfiguration createConfiguration( @NotNull ConfigurationKind kind, @NotNull TestJdkKind jdkKind, @NotNull List classpath, @NotNull List javaSource, @NotNull List testFilesWithConfigurationDirectives ) { CompilerConfiguration configuration = KotlinTestUtils.newConfiguration(kind, jdkKind, classpath, javaSource); updateConfigurationByDirectivesInTestFiles(testFilesWithConfigurationDirectives, configuration); return configuration; } private static void updateConfigurationByDirectivesInTestFiles( @NotNull List testFilesWithConfigurationDirectives, @NotNull CompilerConfiguration configuration ) { List kotlinConfigurationFlags = new ArrayList(0); for (TestFile testFile : testFilesWithConfigurationDirectives) { kotlinConfigurationFlags.addAll(InTextDirectivesUtils.findListWithPrefixes(testFile.content, "// KOTLIN_CONFIGURATION_FLAGS:")); } updateConfigurationWithFlags(configuration, kotlinConfigurationFlags); } private static final Map> FLAG_NAMESPACE_TO_CLASS = ImmutableMap.of( "CLI", CLIConfigurationKeys.class, "JVM", JVMConfigurationKeys.class ); private static final List> FLAG_CLASSES = ImmutableList.of(CLIConfigurationKeys.class, JVMConfigurationKeys.class); private static final Pattern BOOLEAN_FLAG_PATTERN = Pattern.compile("([+-])(([a-zA-Z_0-9]*)\\.)?([a-zA-Z_0-9]*)"); private static void updateConfigurationWithFlags(@NotNull CompilerConfiguration configuration, @NotNull List flags) { for (String flag : flags) { Matcher m = BOOLEAN_FLAG_PATTERN.matcher(flag); if (m.matches()) { boolean flagEnabled = !"-".equals(m.group(1)); String flagNamespace = m.group(3); String flagName = m.group(4); tryApplyBooleanFlag(configuration, flag, flagEnabled, flagNamespace, flagName); } } } private static void tryApplyBooleanFlag( @NotNull CompilerConfiguration configuration, @NotNull String flag, boolean flagEnabled, @Nullable String flagNamespace, @NotNull String flagName ) { Class configurationKeysClass; Field configurationKeyField = null; if (flagNamespace == null) { for (Class flagClass : FLAG_CLASSES) { try { configurationKeyField = flagClass.getField(flagName); break; } catch (Exception ignored) { } } } else { configurationKeysClass = FLAG_NAMESPACE_TO_CLASS.get(flagNamespace); assert configurationKeysClass != null : "Expected [+|-][namespace.]configurationKey, got: " + flag; try { configurationKeyField = configurationKeysClass.getField(flagName); } catch (Exception e) { configurationKeyField = null; } } assert configurationKeyField != null : "Expected [+|-][namespace.]configurationKey, got: " + flag; try { //noinspection unchecked CompilerConfigurationKey configurationKey = (CompilerConfigurationKey) configurationKeyField.get(null); configuration.put(configurationKey, flagEnabled); } catch (Exception e) { assert false : "Expected [+|-][namespace.]configurationKey, got: " + flag; } } @Override protected void tearDown() throws Exception { myFiles = null; myEnvironment = null; classFileFactory = null; if (initializedClassLoader != null) { initializedClassLoader.dispose(); initializedClassLoader = null; } super.tearDown(); } protected void loadText(@NotNull String text) { myFiles = CodegenTestFiles.create(DEFAULT_TEST_FILE_NAME + ".kt", text, myEnvironment.getProject()); } @NotNull protected String loadFile(@NotNull @TestDataFile String name) { return loadFileByFullPath(KotlinTestUtils.getTestDataPathBase() + "/codegen/" + name); } @NotNull protected String loadFileByFullPath(@NotNull String fullPath) { try { File file = new File(fullPath); String content = FileUtil.loadFile(file, Charsets.UTF_8.name(), true); myFiles = CodegenTestFiles.create(file.getName(), content, myEnvironment.getProject()); return content; } catch (IOException e) { throw new RuntimeException(e); } } protected void loadFiles(@NotNull String... names) { myFiles = CodegenTestFiles.create(myEnvironment.getProject(), names); } protected void loadFile() { loadFile(getPrefix() + "/" + getTestName(true) + ".kt"); } protected void loadMultiFiles(@NotNull List files) { Collections.sort(files); List ktFiles = new ArrayList(files.size()); for (TestFile file : files) { if (file.name.endsWith(".kt")) { String content = CheckerTestUtil.parseDiagnosedRanges(file.content, new ArrayList(0)); ktFiles.add(KotlinTestUtils.createFile(file.name, content, myEnvironment.getProject())); } } myFiles = CodegenTestFiles.create(ktFiles); } @NotNull protected String codegenTestBasePath() { return "compiler/testData/codegen/"; } @NotNull protected String relativePath(@NotNull File file) { String stringToCut = codegenTestBasePath(); String systemIndependentPath = file.getPath().replace(File.separatorChar, '/'); assert systemIndependentPath.startsWith(stringToCut) : "File path is not absolute: " + file; return systemIndependentPath.substring(stringToCut.length()); } @NotNull protected String getPrefix() { throw new UnsupportedOperationException(); } @NotNull protected GeneratedClassLoader generateAndCreateClassLoader() { if (initializedClassLoader != null) { fail("Double initialization of class loader in same test"); } initializedClassLoader = createClassLoader(); if (!verifyAllFilesWithAsm(generateClassesInFile(), initializedClassLoader)) { fail("Verification failed: see exceptions above"); } return initializedClassLoader; } @NotNull protected GeneratedClassLoader createClassLoader() { return new GeneratedClassLoader( generateClassesInFile(), configurationKind.getWithReflection() ? ForTestCompileRuntime.runtimeAndReflectJarClassLoader() : ForTestCompileRuntime.runtimeJarClassLoader(), getClassPathURLs() ); } @NotNull private URL[] getClassPathURLs() { List urls = Lists.newArrayList(); for (File file : JvmContentRootsKt.getJvmClasspathRoots(myEnvironment.getConfiguration())) { try { urls.add(file.toURI().toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } } return urls.toArray(new URL[urls.size()]); } @NotNull protected String generateToText() { if (classFileFactory == null) { classFileFactory = generateFiles(myEnvironment, myFiles); } return classFileFactory.createText(); } @NotNull protected Map generateEachFileToText() { if (classFileFactory == null) { classFileFactory = generateFiles(myEnvironment, myFiles); } return classFileFactory.createTextForEachFile(); } @NotNull protected Class generateFacadeClass() { FqName facadeClassFqName = JvmFileClassUtil.getFileClassInfoNoResolve(myFiles.getPsiFile()).getFacadeClassFqName(); return generateClass(facadeClassFqName.asString()); } @NotNull protected Class generateClass(@NotNull String name) { try { return generateAndCreateClassLoader().loadClass(name); } catch (ClassNotFoundException e) { fail("No class file was generated for: " + name); return null; } } @NotNull protected ClassFileFactory generateClassesInFile() { if (classFileFactory == null) { try { classFileFactory = generateFiles(myEnvironment, myFiles); if (DxChecker.RUN_DX_CHECKER) { DxChecker.check(classFileFactory); } } catch (Throwable e) { e.printStackTrace(); System.err.println("Generating instructions as text..."); try { if (classFileFactory == null) { System.out.println("Cannot generate text: exception was thrown during generation"); } else { System.out.println(classFileFactory.createText()); } } catch (Throwable e1) { System.err.println("Exception thrown while trying to generate text, the actual exception follows:"); e1.printStackTrace(); System.err.println("-----------------------------------------------------------------------------"); } fail("See exceptions above"); } } return classFileFactory; } private static boolean verifyAllFilesWithAsm(ClassFileFactory factory, ClassLoader loader) { boolean noErrors = true; for (OutputFile file : ClassFileUtilsKt.getClassFiles(factory)) { noErrors &= verifyWithAsm(file, loader); } return noErrors; } private static boolean verifyWithAsm(@NotNull OutputFile file, ClassLoader loader) { ClassNode classNode = new ClassNode(); new ClassReader(file.asByteArray()).accept(classNode, 0); SimpleVerifier verifier = new SimpleVerifier(); verifier.setClassLoader(loader); Analyzer analyzer = new Analyzer(verifier); boolean noErrors = true; for (MethodNode method : classNode.methods) { try { analyzer.analyze(classNode.name, method); } catch (Throwable e) { System.err.println(file.asText()); System.err.println(classNode.name + "::" + method.name + method.desc); //noinspection InstanceofCatchParameter if (e instanceof AnalyzerException) { // Print the erroneous instruction TraceMethodVisitor tmv = new TraceMethodVisitor(new Textifier()); ((AnalyzerException) e).node.accept(tmv); PrintWriter pw = new PrintWriter(System.err); tmv.p.print(pw); pw.flush(); } e.printStackTrace(); noErrors = false; } } return noErrors; } @NotNull protected Method generateFunction() { Class aClass = generateFacadeClass(); try { return findTheOnlyMethod(aClass); } catch (Error e) { System.out.println(generateToText()); throw e; } } @NotNull protected Method generateFunction(@NotNull String name) { return findDeclaredMethodByName(generateFacadeClass(), name); } @NotNull public Class loadAnnotationClassQuietly(@NotNull String fqName) { try { //noinspection unchecked return (Class) initializedClassLoader.loadClass(fqName); } catch (ClassNotFoundException e) { throw ExceptionUtilsKt.rethrow(e); } } public static class TestFile implements Comparable { public final String name; public final String content; public TestFile(@NotNull String name, @NotNull String content) { this.name = name; this.content = content; } @Override public int compareTo(@NotNull TestFile o) { return name.compareTo(o.name); } @Override public int hashCode() { return name.hashCode(); } @Override public boolean equals(Object obj) { return obj instanceof TestFile && ((TestFile) obj).name.equals(name); } @Override public String toString() { return name; } } protected void doTest(String filePath) throws Exception { File file = new File(filePath); String expectedText = KotlinTestUtils.doLoadFile(file); Ref javaFilesDir = Ref.create(); List testFiles = createTestFiles(file, expectedText, javaFilesDir); doMultiFileTest(file, testFiles, javaFilesDir.get()); } @NotNull private static List createTestFiles(File file, String expectedText, final Ref javaFilesDir) { return KotlinTestUtils.createTestFiles(file.getName(), expectedText, new KotlinTestUtils.TestFileFactoryNoModules() { @NotNull @Override public TestFile create(@NotNull String fileName, @NotNull String text, @NotNull Map directives) { if (fileName.endsWith(".java")) { if (javaFilesDir.isNull()) { try { javaFilesDir.set(KotlinTestUtils.tmpDir("java-files")); } catch (IOException e) { throw ExceptionUtilsKt.rethrow(e); } } writeSourceFile(fileName, text, javaFilesDir.get()); } return new TestFile(fileName, text); } private void writeSourceFile(@NotNull String fileName, @NotNull String content, @NotNull File targetDir) { File file = new File(targetDir, fileName); KotlinTestUtils.mkdirs(file.getParentFile()); FilesKt.writeText(file, content, Charsets.UTF_8); } }); } protected void doMultiFileTest(@NotNull File wholeFile, @NotNull List files, @Nullable File javaFilesDir) throws Exception { throw new UnsupportedOperationException("Multi-file test cases are not supported in this test"); } }