/* * 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.test; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.intellij.openapi.Disposable; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.ShutDownTracker; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFileFactory; import com.intellij.psi.impl.PsiFileFactoryImpl; import com.intellij.rt.execution.junit.FileComparisonFailure; import com.intellij.testFramework.LightVirtualFile; import com.intellij.testFramework.TestDataFile; import com.intellij.util.containers.ContainerUtil; import junit.framework.TestCase; import kotlin.collections.CollectionsKt; import kotlin.collections.SetsKt; import kotlin.jvm.functions.Function1; import kotlin.text.StringsKt; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import org.jetbrains.kotlin.analyzer.AnalysisResult; import org.jetbrains.kotlin.builtins.DefaultBuiltIns; import org.jetbrains.kotlin.builtins.KotlinBuiltIns; import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys; import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation; import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity; import org.jetbrains.kotlin.cli.common.messages.MessageCollector; 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.*; import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl; import org.jetbrains.kotlin.diagnostics.Diagnostic; import org.jetbrains.kotlin.diagnostics.Errors; import org.jetbrains.kotlin.diagnostics.Severity; import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages; import org.jetbrains.kotlin.idea.KotlinLanguage; import org.jetbrains.kotlin.jvm.compiler.LoadDescriptorUtil; import org.jetbrains.kotlin.lexer.KtTokens; import org.jetbrains.kotlin.name.Name; import org.jetbrains.kotlin.psi.KtExpression; import org.jetbrains.kotlin.psi.KtFile; import org.jetbrains.kotlin.psi.KtPsiFactoryKt; import org.jetbrains.kotlin.resolve.BindingContext; import org.jetbrains.kotlin.resolve.BindingTrace; import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics; import org.jetbrains.kotlin.resolve.lazy.JvmResolveUtil; import org.jetbrains.kotlin.storage.LockBasedStorageManager; import org.jetbrains.kotlin.test.util.JetTestUtilsKt; import org.jetbrains.kotlin.types.KotlinType; import org.jetbrains.kotlin.types.expressions.KotlinTypeInfo; import org.jetbrains.kotlin.util.slicedMap.ReadOnlySlice; import org.jetbrains.kotlin.util.slicedMap.SlicedMap; import org.jetbrains.kotlin.util.slicedMap.WritableSlice; import org.jetbrains.kotlin.utils.ExceptionUtilsKt; import org.jetbrains.kotlin.utils.PathUtil; import org.junit.Assert; import javax.tools.*; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import static org.jetbrains.kotlin.test.InTextDirectivesUtils.isCompatibleTarget; import static org.jetbrains.kotlin.test.InTextDirectivesUtils.isDirectiveDefined; public class KotlinTestUtils { public static String TEST_MODULE_NAME = "test-module"; public static final String TEST_GENERATOR_NAME = "org.jetbrains.kotlin.generators.tests.TestsPackage"; public static final String PLEASE_REGENERATE_TESTS = "Please regenerate tests (GenerateTests.kt)"; private static final List<File> filesToDelete = new ArrayList<>(); /** * Syntax: * * // MODULE: name(dependency1, dependency2, ...) * * // FILE: name * * Several files may follow one module */ private static final String MODULE_DELIMITER = ",\\s*"; private static final Pattern FILE_OR_MODULE_PATTERN = Pattern.compile( "(?://\\s*MODULE:\\s*([^()\\n]+)(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*(?:\\(([^()]+(?:" + MODULE_DELIMITER + "[^()]+)*)\\))?\\s*)?" + "//\\s*FILE:\\s*(.*)$", Pattern.MULTILINE); private static final Pattern DIRECTIVE_PATTERN = Pattern.compile("^//\\s*!([\\w_]+)(:\\s*(.*)$)?", Pattern.MULTILINE); public static final BindingTrace DUMMY_TRACE = new BindingTrace() { @NotNull @Override public BindingContext getBindingContext() { return new BindingContext() { @NotNull @Override public Diagnostics getDiagnostics() { return Diagnostics.Companion.getEMPTY(); } @Override public <K, V> V get(ReadOnlySlice<K, V> slice, K key) { return DUMMY_TRACE.get(slice, key); } @NotNull @Override public <K, V> Collection<K> getKeys(WritableSlice<K, V> slice) { return DUMMY_TRACE.getKeys(slice); } @NotNull @TestOnly @Override public <K, V> ImmutableMap<K, V> getSliceContents(@NotNull ReadOnlySlice<K, V> slice) { return ImmutableMap.of(); } @Nullable @Override public KotlinType getType(@NotNull KtExpression expression) { return DUMMY_TRACE.getType(expression); } @Override public void addOwnDataTo(@NotNull BindingTrace trace, boolean commitDiagnostics) { // do nothing } }; } @Override public <K, V> void record(WritableSlice<K, V> slice, K key, V value) { } @Override public <K> void record(WritableSlice<K, Boolean> slice, K key) { } @Override @SuppressWarnings("unchecked") public <K, V> V get(ReadOnlySlice<K, V> slice, K key) { if (slice == BindingContext.PROCESSED) return (V) Boolean.FALSE; return SlicedMap.DO_NOTHING.get(slice, key); } @NotNull @Override public <K, V> Collection<K> getKeys(WritableSlice<K, V> slice) { assert slice.isCollective(); return Collections.emptySet(); } @Nullable @Override public KotlinType getType(@NotNull KtExpression expression) { KotlinTypeInfo typeInfo = get(BindingContext.EXPRESSION_TYPE_INFO, expression); return typeInfo != null ? typeInfo.getType() : null; } @Override public void recordType(@NotNull KtExpression expression, @Nullable KotlinType type) { } @Override public void report(@NotNull Diagnostic diagnostic) { if (Errors.UNRESOLVED_REFERENCE_DIAGNOSTICS.contains(diagnostic.getFactory())) { throw new IllegalStateException("Unresolved: " + diagnostic.getPsiElement().getText()); } } @Override public boolean wantsDiagnostics() { return false; } }; public static BindingTrace DUMMY_EXCEPTION_ON_ERROR_TRACE = new BindingTrace() { @NotNull @Override public BindingContext getBindingContext() { return new BindingContext() { @NotNull @Override public Diagnostics getDiagnostics() { throw new UnsupportedOperationException(); } @Override public <K, V> V get(ReadOnlySlice<K, V> slice, K key) { return DUMMY_EXCEPTION_ON_ERROR_TRACE.get(slice, key); } @NotNull @Override public <K, V> Collection<K> getKeys(WritableSlice<K, V> slice) { return DUMMY_EXCEPTION_ON_ERROR_TRACE.getKeys(slice); } @NotNull @TestOnly @Override public <K, V> ImmutableMap<K, V> getSliceContents(@NotNull ReadOnlySlice<K, V> slice) { return ImmutableMap.of(); } @Nullable @Override public KotlinType getType(@NotNull KtExpression expression) { return DUMMY_EXCEPTION_ON_ERROR_TRACE.getType(expression); } @Override public void addOwnDataTo(@NotNull BindingTrace trace, boolean commitDiagnostics) { // do nothing } }; } @Override public <K, V> void record(WritableSlice<K, V> slice, K key, V value) { } @Override public <K> void record(WritableSlice<K, Boolean> slice, K key) { } @Override public <K, V> V get(ReadOnlySlice<K, V> slice, K key) { return null; } @NotNull @Override public <K, V> Collection<K> getKeys(WritableSlice<K, V> slice) { assert slice.isCollective(); return Collections.emptySet(); } @Nullable @Override public KotlinType getType(@NotNull KtExpression expression) { return null; } @Override public void recordType(@NotNull KtExpression expression, @Nullable KotlinType type) { } @Override public void report(@NotNull Diagnostic diagnostic) { if (diagnostic.getSeverity() == Severity.ERROR) { throw new IllegalStateException(DefaultErrorMessages.render(diagnostic)); } } @Override public boolean wantsDiagnostics() { return true; } }; // We suspect sequences of eight consecutive hexadecimal digits to be a package part hash code private static final Pattern STRIP_PACKAGE_PART_HASH_PATTERN = Pattern.compile("\\$([0-9a-f]{8})"); private KotlinTestUtils() { } @NotNull public static AnalysisResult analyzeFile(@NotNull KtFile file, @NotNull KotlinCoreEnvironment environment) { return JvmResolveUtil.analyze(file, environment); } @NotNull public static KotlinCoreEnvironment createEnvironmentWithMockJdkAndIdeaAnnotations(Disposable disposable) { return createEnvironmentWithMockJdkAndIdeaAnnotations(disposable, ConfigurationKind.ALL); } @NotNull public static KotlinCoreEnvironment createEnvironmentWithMockJdkAndIdeaAnnotations(Disposable disposable, @NotNull ConfigurationKind configurationKind) { return createEnvironmentWithJdkAndNullabilityAnnotationsFromIdea(disposable, configurationKind, TestJdkKind.MOCK_JDK); } @NotNull public static KotlinCoreEnvironment createEnvironmentWithJdkAndNullabilityAnnotationsFromIdea( @NotNull Disposable disposable, @NotNull ConfigurationKind configurationKind, @NotNull TestJdkKind jdkKind ) { return KotlinCoreEnvironment.createForTests( disposable, newConfiguration(configurationKind, jdkKind, getAnnotationsJar()), EnvironmentConfigFiles.JVM_CONFIG_FILES ); } @NotNull public static String getTestDataPathBase() { return getHomeDirectory() + "/compiler/testData"; } @NotNull public static String getHomeDirectory() { File resourceRoot = PathUtil.getResourcePathForClass(KotlinTestUtils.class); return FileUtil.toSystemIndependentName(resourceRoot.getParentFile().getParentFile().getParent()); } public static File findMockJdkRtJar() { return new File(getHomeDirectory(), "compiler/testData/mockJDK/jre/lib/rt.jar"); } // Differs from common mock JDK only by one additional 'nonExistingMethod' in Collection and constructor from Double in Throwable // It's needed to test the way we load additional built-ins members that neither in black nor white lists public static File findMockJdkRtModified() { return new File(getHomeDirectory(), "compiler/testData/mockJDKModified/rt.jar"); } public static File findAndroidApiJar() { return new File(getHomeDirectory(), "dependencies/android.jar"); } public static File getAnnotationsJar() { return new File(getHomeDirectory(), "compiler/testData/mockJDK/jre/lib/annotations.jar"); } public static void mkdirs(@NotNull File file) { if (file.isDirectory()) { return; } if (!file.mkdirs()) { if (file.exists()) { throw new IllegalStateException("Failed to create " + file + ": file exists and not a directory"); } throw new IllegalStateException("Failed to create " + file); } } @NotNull public static File tmpDirForTest(TestCase test) throws IOException { File answer = FileUtil.createTempDirectory(test.getClass().getSimpleName(), test.getName()); deleteOnShutdown(answer); return answer; } @NotNull public static File tmpDir(String name) throws IOException { // we should use this form. otherwise directory will be deleted on each test File answer = FileUtil.createTempDirectory(new File(System.getProperty("java.io.tmpdir")), name, ""); deleteOnShutdown(answer); return answer; } public static void deleteOnShutdown(File file) { if (filesToDelete.isEmpty()) { ShutDownTracker.getInstance().registerShutdownTask(() -> ShutDownTracker.invokeAndWait(true, true, () -> { for (File victim : filesToDelete) { FileUtil.delete(victim); } })); } filesToDelete.add(file); } @NotNull public static KtFile createFile(@NotNull @NonNls String name, @NotNull String text, @NotNull Project project) { String shortName = name.substring(name.lastIndexOf('/') + 1); shortName = shortName.substring(shortName.lastIndexOf('\\') + 1); LightVirtualFile virtualFile = new LightVirtualFile(shortName, KotlinLanguage.INSTANCE, text) { @NotNull @Override public String getPath() { //TODO: patch LightVirtualFile return "/" + name; } }; virtualFile.setCharset(CharsetToolkit.UTF8_CHARSET); PsiFileFactoryImpl factory = (PsiFileFactoryImpl) PsiFileFactory.getInstance(project); //noinspection ConstantConditions return (KtFile) factory.trySetupPsiForFile(virtualFile, KotlinLanguage.INSTANCE, true, false); } public static String doLoadFile(String myFullDataPath, String name) throws IOException { String fullName = myFullDataPath + File.separatorChar + name; return doLoadFile(new File(fullName)); } public static String doLoadFile(@NotNull File file) throws IOException { return FileUtil.loadFile(file, CharsetToolkit.UTF8, true); } public static String getFilePath(File file) { return FileUtil.toSystemIndependentName(file.getPath()); } @NotNull public static CompilerConfiguration newConfiguration() { CompilerConfiguration configuration = new CompilerConfiguration(); configuration.put(CommonConfigurationKeys.MODULE_NAME, TEST_MODULE_NAME); configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, new MessageCollector() { @Override public void clear() { } @Override public void report( @NotNull CompilerMessageSeverity severity, @NotNull String message, @Nullable CompilerMessageLocation location ) { if (severity == CompilerMessageSeverity.ERROR) { String prefix = location == null ? "" : "(" + location.getPath() + ":" + location.getLine() + ":" + location.getColumn() + ") "; throw new AssertionError(prefix + message); } } @Override public boolean hasErrors() { return false; } }); return configuration; } @NotNull public static CompilerConfiguration newConfiguration( @NotNull ConfigurationKind configurationKind, @NotNull TestJdkKind jdkKind, @NotNull File... extraClasspath ) { return newConfiguration(configurationKind, jdkKind, Arrays.asList(extraClasspath), Collections.emptyList()); } @NotNull public static CompilerConfiguration newConfiguration( @NotNull ConfigurationKind configurationKind, @NotNull TestJdkKind jdkKind, @NotNull List<File> classpath, @NotNull List<File> javaSource ) { CompilerConfiguration configuration = newConfiguration(); JvmContentRootsKt.addJavaSourceRoots(configuration, javaSource); if (jdkKind == TestJdkKind.MOCK_JDK) { JvmContentRootsKt.addJvmClasspathRoot(configuration, findMockJdkRtJar()); } else if (jdkKind == TestJdkKind.MODIFIED_MOCK_JDK) { JvmContentRootsKt.addJvmClasspathRoot(configuration, findMockJdkRtModified()); } else if (jdkKind == TestJdkKind.ANDROID_API) { JvmContentRootsKt.addJvmClasspathRoot(configuration, findAndroidApiJar()); } else if (jdkKind == TestJdkKind.FULL_JDK_6) { String jdk6 = System.getenv("JDK_16"); assert jdk6 != null : "Environment variable JDK_16 is not set"; JvmContentRootsKt.addJvmClasspathRoots(configuration, PathUtil.getJdkClassesRootsFromJre(getJreHome(jdk6))); } else if (jdkKind == TestJdkKind.FULL_JDK_9) { String jdk9 = System.getenv("JDK_9"); if (jdk9 != null) { configuration.put(JVMConfigurationKeys.JDK_HOME, new File(getJreHome(jdk9))); } else { System.err.println("Environment variable JDK_9 is not set, the test will be skipped"); } } else { JvmContentRootsKt.addJvmClasspathRoots(configuration, PathUtil.getJdkClassesRootsFromCurrentJre()); } if (configurationKind.getWithRuntime()) { JvmContentRootsKt.addJvmClasspathRoot(configuration, ForTestCompileRuntime.runtimeJarForTests()); JvmContentRootsKt.addJvmClasspathRoot(configuration, ForTestCompileRuntime.scriptRuntimeJarForTests()); JvmContentRootsKt.addJvmClasspathRoot(configuration, ForTestCompileRuntime.kotlinTestJarForTests()); } else if (configurationKind.getWithMockRuntime()) { JvmContentRootsKt.addJvmClasspathRoot(configuration, ForTestCompileRuntime.mockRuntimeJarForTests()); JvmContentRootsKt.addJvmClasspathRoot(configuration, ForTestCompileRuntime.scriptRuntimeJarForTests()); } if (configurationKind.getWithReflection()) { JvmContentRootsKt.addJvmClasspathRoot(configuration, ForTestCompileRuntime.reflectJarForTests()); } JvmContentRootsKt.addJvmClasspathRoots(configuration, classpath); return configuration; } @NotNull private static String getJreHome(@NotNull String jdkHome) { File jre = new File(jdkHome, "jre"); return jre.isDirectory() ? jre.getPath() : jdkHome; } public static void resolveAllKotlinFiles(KotlinCoreEnvironment environment) throws IOException { List<ContentRoot> paths = environment.getConfiguration().get(JVMConfigurationKeys.CONTENT_ROOTS); if (paths == null) return; List<KtFile> ktFiles = new ArrayList<>(); for (ContentRoot root : paths) { if (!(root instanceof KotlinSourceRoot)) continue; String path = ((KotlinSourceRoot) root).getPath(); File file = new File(path); if (file.isFile()) { ktFiles.add(loadJetFile(environment.getProject(), file)); } else { //noinspection ConstantConditions for (File childFile : file.listFiles()) { if (childFile.getName().endsWith(".kt")) { ktFiles.add(loadJetFile(environment.getProject(), childFile)); } } } } JvmResolveUtil.analyze(ktFiles, environment); } public static void assertEqualsToFile(@NotNull File expectedFile, @NotNull Editor editor) { String actualText = editor.getDocument().getText(); String afterText = new StringBuilder(actualText).insert(editor.getCaretModel().getOffset(), "<caret>").toString(); assertEqualsToFile(expectedFile, afterText); } public static void assertEqualsToFile(@NotNull File expectedFile, @NotNull String actual) { assertEqualsToFile(expectedFile, actual, s -> s); } public static void assertEqualsToFile(@NotNull File expectedFile, @NotNull String actual, @NotNull Function1<String, String> sanitizer) { try { String actualText = JetTestUtilsKt.trimTrailingWhitespacesAndAddNewlineAtEOF(StringUtil.convertLineSeparators(actual.trim())); if (!expectedFile.exists()) { FileUtil.writeToFile(expectedFile, actualText); Assert.fail("Expected data file did not exist. Generating: " + expectedFile); } String expected = FileUtil.loadFile(expectedFile, CharsetToolkit.UTF8, true); String expectedText = JetTestUtilsKt.trimTrailingWhitespacesAndAddNewlineAtEOF(StringUtil.convertLineSeparators(expected.trim())); if (!Comparing.equal(sanitizer.invoke(expectedText), sanitizer.invoke(actualText))) { throw new FileComparisonFailure("Actual data differs from file content: " + expectedFile.getName(), expected, actual, expectedFile.getAbsolutePath()); } } catch (IOException e) { throw ExceptionUtilsKt.rethrow(e); } } public static boolean compileKotlinWithJava( @NotNull List<File> javaFiles, @NotNull List<File> ktFiles, @NotNull File outDir, @NotNull Disposable disposable, @Nullable File javaErrorFile ) throws IOException { if (!ktFiles.isEmpty()) { KotlinCoreEnvironment environment = createEnvironmentWithMockJdkAndIdeaAnnotations(disposable); LoadDescriptorUtil.compileKotlinToDirAndGetModule(ktFiles, outDir, environment); } else { boolean mkdirs = outDir.mkdirs(); assert mkdirs : "Not created: " + outDir; } if (javaFiles.isEmpty()) return true; return compileJavaFiles(javaFiles, Arrays.asList( "-classpath", outDir.getPath() + File.pathSeparator + ForTestCompileRuntime.runtimeJarForTests(), "-d", outDir.getPath() ), javaErrorFile); } public interface TestFileFactory<M, F> { F createFile(@Nullable M module, @NotNull String fileName, @NotNull String text, @NotNull Map<String, String> directives); M createModule(@NotNull String name, @NotNull List<String> dependencies, @NotNull List<String> friends); } public static abstract class TestFileFactoryNoModules<F> implements TestFileFactory<Void, F> { @Override public final F createFile( @Nullable Void module, @NotNull String fileName, @NotNull String text, @NotNull Map<String, String> directives ) { return create(fileName, text, directives); } @NotNull public abstract F create(@NotNull String fileName, @NotNull String text, @NotNull Map<String, String> directives); @Override public Void createModule(@NotNull String name, @NotNull List<String> dependencies, @NotNull List<String> friends) { return null; } } @NotNull public static <M, F> List<F> createTestFiles(String testFileName, String expectedText, TestFileFactory<M, F> factory) { Map<String, String> directives = parseDirectives(expectedText); List<F> testFiles = Lists.newArrayList(); Matcher matcher = FILE_OR_MODULE_PATTERN.matcher(expectedText); boolean hasModules = false; if (!matcher.find()) { // One file testFiles.add(factory.createFile(null, testFileName, expectedText, directives)); } else { int processedChars = 0; M module = null; // Many files while (true) { String moduleName = matcher.group(1); String moduleDependencies = matcher.group(2); String moduleFriends = matcher.group(3); if (moduleName != null) { hasModules = true; module = factory.createModule(moduleName, parseModuleList(moduleDependencies), parseModuleList(moduleFriends)); } String fileName = matcher.group(4); int start = processedChars; boolean nextFileExists = matcher.find(); int end; if (nextFileExists) { end = matcher.start(); } else { end = expectedText.length(); } String fileText = expectedText.substring(start, end); processedChars = end; testFiles.add(factory.createFile(module, fileName, fileText, directives)); if (!nextFileExists) break; } assert processedChars == expectedText.length() : "Characters skipped from " + processedChars + " to " + (expectedText.length() - 1); } if (isDirectiveDefined(expectedText, "WITH_COROUTINES")) { M supportModule = hasModules ? factory.createModule("support", Collections.emptyList(), Collections.emptyList()) : null; testFiles.add(factory.createFile(supportModule, "CoroutineUtil.kt", "package helpers\n" + "import kotlin.coroutines.experimental.*\n" + "fun <T> handleResultContinuation(x: (T) -> Unit): Continuation<T> = object: Continuation<T> {\n" + " override val context = EmptyCoroutineContext\n" + " override fun resumeWithException(exception: Throwable) {\n" + " throw exception\n" + " }\n" + "\n" + " override fun resume(data: T) = x(data)\n" + "}\n" + "\n" + "fun handleExceptionContinuation(x: (Throwable) -> Unit): Continuation<Any?> = object: Continuation<Any?> {\n" + " override val context = EmptyCoroutineContext\n" + " override fun resumeWithException(exception: Throwable) {\n" + " x(exception)\n" + " }\n" + "\n" + " override fun resume(data: Any?) { }\n" + "}\n" + "\n" + "open class EmptyContinuation(override val context: CoroutineContext = EmptyCoroutineContext) : Continuation<Any?> {\n" + " companion object : EmptyContinuation()\n" + " override fun resume(data: Any?) {}\n" + " override fun resumeWithException(exception: Throwable) { throw exception }\n" + "}", directives )); } return testFiles; } private static List<String> parseModuleList(@Nullable String dependencies) { if (dependencies == null) return Collections.emptyList(); return StringsKt.split(dependencies, Pattern.compile(MODULE_DELIMITER), 0); } @NotNull public static Map<String, String> parseDirectives(String expectedText) { Map<String, String> directives = Maps.newHashMap(); Matcher directiveMatcher = DIRECTIVE_PATTERN.matcher(expectedText); int start = 0; while (directiveMatcher.find()) { if (directiveMatcher.start() != start) { Assert.fail("Directives should only occur at the beginning of a file: " + directiveMatcher.group()); } String name = directiveMatcher.group(1); String value = directiveMatcher.group(3); String oldValue = directives.put(name, value); Assert.assertNull("Directive overwritten: " + name + " old value: " + oldValue + " new value: " + value, oldValue); start = directiveMatcher.end() + 1; } return directives; } public static List<String> loadBeforeAfterText(String filePath) { String content; try { content = FileUtil.loadFile(new File(filePath), true); } catch (IOException e) { throw new RuntimeException(e); } List<String> files = createTestFiles("", content, new TestFileFactoryNoModules<String>() { @NotNull @Override public String create(@NotNull String fileName, @NotNull String text, @NotNull Map<String, String> directives) { int firstLineEnd = text.indexOf('\n'); return StringUtil.trimTrailing(text.substring(firstLineEnd + 1)); } }); Assert.assertTrue("Exactly two files expected: ", files.size() == 2); return files; } public static String getLastCommentedLines(@NotNull Document document) { List<CharSequence> resultLines = new ArrayList<>(); for (int i = document.getLineCount() - 1; i >= 0; i--) { int lineStart = document.getLineStartOffset(i); int lineEnd = document.getLineEndOffset(i); if (document.getCharsSequence().subSequence(lineStart, lineEnd).toString().trim().isEmpty()) { continue; } if ("//".equals(document.getCharsSequence().subSequence(lineStart, lineStart + 2).toString())) { resultLines.add(document.getCharsSequence().subSequence(lineStart + 2, lineEnd)); } else { break; } } Collections.reverse(resultLines); StringBuilder result = new StringBuilder(); for (CharSequence line : resultLines) { result.append(line).append("\n"); } result.delete(result.length() - 1, result.length()); return result.toString(); } public enum CommentType { ALL, LINE_COMMENT, BLOCK_COMMENT } @NotNull public static String getLastCommentInFile(@NotNull KtFile file) { return CollectionsKt.first(getLastCommentsInFile(file, CommentType.ALL, true)); } @NotNull public static List<String> getLastCommentsInFile(@NotNull KtFile file, CommentType commentType, boolean assertMustExist) { PsiElement lastChild = file.getLastChild(); if (lastChild != null && lastChild.getNode().getElementType().equals(KtTokens.WHITE_SPACE)) { lastChild = lastChild.getPrevSibling(); } assert lastChild != null; List<String> comments = ContainerUtil.newArrayList(); while (true) { if (lastChild.getNode().getElementType().equals(KtTokens.BLOCK_COMMENT)) { if (commentType == CommentType.ALL || commentType == CommentType.BLOCK_COMMENT) { String lastChildText = lastChild.getText(); comments.add(lastChildText.substring(2, lastChildText.length() - 2).trim()); } } else if (lastChild.getNode().getElementType().equals(KtTokens.EOL_COMMENT)) { if (commentType == CommentType.ALL || commentType == CommentType.LINE_COMMENT) { comments.add(lastChild.getText().substring(2).trim()); } } else { break; } lastChild = lastChild.getPrevSibling(); } if (comments.isEmpty() && assertMustExist) { throw new AssertionError(String.format( "Test file '%s' should end in a comment of type %s; last node was: %s", file.getName(), commentType, lastChild)); } return comments; } public static boolean compileJavaFiles(@NotNull Collection<File> files, List<String> options) throws IOException { return compileJavaFiles(files, options, null); } private static boolean compileJavaFiles(@NotNull Collection<File> files, List<String> options, @Nullable File javaErrorFile) throws IOException { JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>(); try (StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(diagnosticCollector, Locale.ENGLISH, Charset.forName("utf-8"))) { Iterable<? extends JavaFileObject> javaFileObjectsFromFiles = fileManager.getJavaFileObjectsFromFiles(files); JavaCompiler.CompilationTask task = javaCompiler.getTask( new StringWriter(), // do not write to System.err fileManager, diagnosticCollector, options, null, javaFileObjectsFromFiles); Boolean success = task.call(); // do NOT inline this variable, call() should complete before errorsToString() if (javaErrorFile == null || !javaErrorFile.exists()) { Assert.assertTrue(errorsToString(diagnosticCollector, true), success); } else { assertEqualsToFile(javaErrorFile, errorsToString(diagnosticCollector, false)); } return success; } } @NotNull private static String errorsToString(@NotNull DiagnosticCollector<JavaFileObject> diagnosticCollector, boolean humanReadable) { StringBuilder builder = new StringBuilder(); for (javax.tools.Diagnostic<? extends JavaFileObject> diagnostic : diagnosticCollector.getDiagnostics()) { if (diagnostic.getKind() != javax.tools.Diagnostic.Kind.ERROR) continue; if (humanReadable) { builder.append(diagnostic).append("\n"); } else { builder.append(new File(diagnostic.getSource().toUri()).getName()).append(":") .append(diagnostic.getLineNumber()).append(":") .append(diagnostic.getColumnNumber()).append(":") .append(diagnostic.getCode()).append("\n"); } } return builder.toString(); } public static String navigationMetadata(@TestDataFile String testFile) { return testFile; } public static String getTestsRoot(@NotNull Class<?> testCaseClass) { TestMetadata testClassMetadata = testCaseClass.getAnnotation(TestMetadata.class); Assert.assertNotNull("No metadata for class: " + testCaseClass, testClassMetadata); return testClassMetadata.value(); } /** * @return test data file name specified in the metadata of test method */ @Nullable public static String getTestDataFileName(@NotNull Class<?> testCaseClass, @NotNull String testName) { try { Method method = testCaseClass.getDeclaredMethod(testName); return getMethodMetadata(method); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } public static void assertAllTestsPresentByMetadata( @NotNull Class<?> testCaseClass, @NotNull File testDataDir, @NotNull Pattern filenamePattern, @NotNull TargetBackend targetBackend, boolean recursive, @NotNull String... excludeDirs ) { File rootFile = new File(getTestsRoot(testCaseClass)); Set<String> filePaths = collectPathsMetadata(testCaseClass); Set<String> exclude = SetsKt.setOf(excludeDirs); File[] files = testDataDir.listFiles(); if (files != null) { for (File file : files) { if (file.isDirectory()) { if (recursive && containsTestData(file, filenamePattern) && !exclude.contains(file.getName())) { assertTestClassPresentByMetadata(testCaseClass, file); } } else if (filenamePattern.matcher(file.getName()).matches() && isCompatibleTarget(targetBackend, file)) { assertFilePathPresent(file, rootFile, filePaths); } } } } public static void assertAllTestsPresentInSingleGeneratedClass( @NotNull Class<?> testCaseClass, @NotNull File testDataDir, @NotNull Pattern filenamePattern, @NotNull TargetBackend targetBackend ) { File rootFile = new File(getTestsRoot(testCaseClass)); Set<String> filePaths = collectPathsMetadata(testCaseClass); FileUtil.processFilesRecursively(testDataDir, file -> { if (file.isFile() && filenamePattern.matcher(file.getName()).matches() && isCompatibleTarget(targetBackend, file)) { assertFilePathPresent(file, rootFile, filePaths); } return true; }); } private static void assertFilePathPresent(File file, File rootFile, Set<String> filePaths) { String path = FileUtil.getRelativePath(rootFile, file); if (path != null) { String relativePath = nameToCompare(path); if (!filePaths.contains(relativePath)) { Assert.fail("Test data file missing from the generated test class: " + file + "\n" + PLEASE_REGENERATE_TESTS); } } } private static Set<String> collectPathsMetadata(Class<?> testCaseClass) { return ContainerUtil.newHashSet(ContainerUtil.map(collectMethodsMetadata(testCaseClass), KotlinTestUtils::nameToCompare)); } @Nullable private static String getMethodMetadata(Method method) { TestMetadata testMetadata = method.getAnnotation(TestMetadata.class); return (testMetadata != null) ? testMetadata.value() : null; } private static Set<String> collectMethodsMetadata(Class<?> testCaseClass) { Set<String> filePaths = Sets.newHashSet(); for (Method method : testCaseClass.getDeclaredMethods()) { String path = getMethodMetadata(method); if (path != null) { filePaths.add(path); } } return filePaths; } private static boolean containsTestData(File dir, Pattern filenamePattern) { File[] files = dir.listFiles(); assert files != null; for (File file : files) { if (file.isDirectory()) { if (containsTestData(file, filenamePattern)) { return true; } } else { if (filenamePattern.matcher(file.getName()).matches()) { return true; } } } return false; } private static void assertTestClassPresentByMetadata(@NotNull Class<?> outerClass, @NotNull File testDataDir) { for (Class<?> nestedClass : outerClass.getDeclaredClasses()) { TestMetadata testMetadata = nestedClass.getAnnotation(TestMetadata.class); if (testMetadata != null && testMetadata.value().equals(getFilePath(testDataDir))) { return; } } Assert.fail("Test data directory missing from the generated test class: " + testDataDir + "\n" + PLEASE_REGENERATE_TESTS); } @NotNull public static KtFile loadJetFile(@NotNull Project project, @NotNull File ioFile) throws IOException { String text = FileUtil.loadFile(ioFile, true); return KtPsiFactoryKt.KtPsiFactory(project).createPhysicalFile(ioFile.getName(), text); } @NotNull public static List<KtFile> loadToJetFiles(@NotNull KotlinCoreEnvironment environment, @NotNull List<File> files) throws IOException { List<KtFile> jetFiles = Lists.newArrayList(); for (File file : files) { jetFiles.add(loadJetFile(environment.getProject(), file)); } return jetFiles; } @NotNull public static ModuleDescriptorImpl createEmptyModule() { return createEmptyModule("<empty-for-test>"); } @NotNull public static ModuleDescriptorImpl createEmptyModule(@NotNull String name) { return createEmptyModule(name, DefaultBuiltIns.getInstance()); } @NotNull public static ModuleDescriptorImpl createEmptyModule(@NotNull String name, @NotNull KotlinBuiltIns builtIns) { return new ModuleDescriptorImpl(Name.special(name), LockBasedStorageManager.NO_LOCKS, builtIns); } @NotNull public static File replaceExtension(@NotNull File file, @Nullable String newExtension) { return new File(file.getParentFile(), FileUtil.getNameWithoutExtension(file) + (newExtension == null ? "" : "." + newExtension)); } @NotNull public static String replaceHashWithStar(@NotNull String string) { return replaceHash(string, "*"); } public static String replaceHash(@NotNull String string, @NotNull String replacement) { //TODO: hashes are still used in SamWrapperCodegen Matcher matcher = STRIP_PACKAGE_PART_HASH_PATTERN.matcher(string); if (matcher.find()) { return matcher.replaceAll("\\$" + replacement); } return string; } public static boolean isAllFilesPresentTest(String testName) { //noinspection SpellCheckingInspection return testName.toLowerCase().startsWith("allfilespresentin"); } public static String nameToCompare(@NotNull String name) { return (SystemInfo.isFileSystemCaseSensitive ? name : name.toLowerCase()).replace('\\', '/'); } }