Java tutorial
/* * Copyright (C) 2016 The Android Open Source Project * * 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.android.tools.idea.testartifacts.instrumented; import com.android.builder.model.AndroidArtifact; import com.android.builder.model.Variant; import com.android.ddmlib.IDevice; import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; import com.android.tools.idea.gradle.project.model.AndroidModuleModel; import com.android.tools.idea.run.*; import com.android.tools.idea.run.editor.AndroidRunConfigurationEditor; import com.android.tools.idea.run.editor.TestRunParameters; import com.android.tools.idea.run.tasks.LaunchTask; import com.android.tools.idea.run.util.LaunchStatus; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.intellij.codeInsight.AnnotationUtil; import com.intellij.execution.*; import com.intellij.execution.configurations.*; import com.intellij.execution.junit.JUnitUtil; import com.intellij.execution.process.ProcessHandler; import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil; import com.intellij.execution.ui.ConsoleView; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.refactoring.listeners.RefactoringElementAdapter; import com.intellij.refactoring.listeners.RefactoringElementListener; import org.jetbrains.android.dom.manifest.Instrumentation; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidFacetConfiguration; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * User: Eugene.Kudelevsky * Date: Aug 27, 2009 * Time: 2:23:56 PM */ public class AndroidTestRunConfiguration extends AndroidRunConfigurationBase implements RefactoringListenerProvider { private static final Logger LOG = Logger.getInstance(AndroidTestRunConfiguration.class); public static final int TEST_ALL_IN_MODULE = 0; public static final int TEST_ALL_IN_PACKAGE = 1; public static final int TEST_CLASS = 2; public static final int TEST_METHOD = 3; public int TESTING_TYPE = TEST_ALL_IN_MODULE; public String INSTRUMENTATION_RUNNER_CLASS = ""; public String METHOD_NAME = ""; public String CLASS_NAME = ""; public String PACKAGE_NAME = ""; public String EXTRA_OPTIONS = ""; public AndroidTestRunConfiguration(final Project project, final ConfigurationFactory factory) { super(project, factory, true); } @Override protected Pair<Boolean, String> supportsRunningLibraryProjects(@NotNull AndroidFacet facet) { if (!facet.requiresAndroidModel()) { // Non Gradle projects always require an application return Pair.create(Boolean.FALSE, AndroidBundle.message("android.cannot.run.library.project.error")); } // TODO: Resolve direct AndroidGradleModel dep (b/22596984) AndroidModuleModel androidModel = AndroidModuleModel.get(facet); if (androidModel == null) { return Pair.create(Boolean.FALSE, AndroidBundle.message("android.cannot.run.library.project.error")); } // Gradle only supports testing against a single build type (which could be anything, but is "debug" build type by default) // Currently, the only information the model exports that we can use to detect whether the current build type // is testable is by looking at the test task name and checking whether it is null. AndroidArtifact testArtifact = androidModel.getAndroidTestArtifactInSelectedVariant(); String testTask = testArtifact != null ? testArtifact.getAssembleTaskName() : null; return new Pair<Boolean, String>(testTask != null, AndroidBundle.message("android.cannot.run.library.project.in.this.buildtype")); } @Override public boolean isGeneratedName() { final String name = getName(); if ((TESTING_TYPE == TEST_CLASS || TESTING_TYPE == TEST_METHOD) && (CLASS_NAME == null || CLASS_NAME.length() == 0)) { return JavaExecutionUtil.isNewName(name); } if (TESTING_TYPE == TEST_METHOD && (METHOD_NAME == null || METHOD_NAME.length() == 0)) { return JavaExecutionUtil.isNewName(name); } return Comparing.equal(name, suggestedName()); } @Override public String suggestedName() { if (TESTING_TYPE == TEST_ALL_IN_PACKAGE) { return ExecutionBundle.message("test.in.scope.presentable.text", PACKAGE_NAME); } else if (TESTING_TYPE == TEST_CLASS) { return ProgramRunnerUtil.shortenName(JavaExecutionUtil.getShortClassName(CLASS_NAME), 0); } else if (TESTING_TYPE == TEST_METHOD) { return ProgramRunnerUtil.shortenName(METHOD_NAME, 2) + "()"; } return ExecutionBundle.message("all.tests.scope.presentable.text"); } @NotNull @Override public List<ValidationError> checkConfiguration(@NotNull AndroidFacet facet) { List<ValidationError> errors = Lists.newArrayList(); Module module = facet.getModule(); JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject()); switch (TESTING_TYPE) { case TEST_ALL_IN_PACKAGE: final PsiPackage testPackage = facade.findPackage(PACKAGE_NAME); if (testPackage == null) { errors.add(ValidationError .warning(ExecutionBundle.message("package.does.not.exist.error.message", PACKAGE_NAME))); } break; case TEST_CLASS: PsiClass testClass = null; try { testClass = getConfigurationModule().checkModuleAndClassName(CLASS_NAME, ExecutionBundle.message("no.test.class.specified.error.text")); } catch (RuntimeConfigurationException e) { errors.add(ValidationError.fromException(e)); } if (testClass != null && !JUnitUtil.isTestClass(testClass)) { errors.add(ValidationError .warning(ExecutionBundle.message("class.isnt.test.class.error.message", CLASS_NAME))); } break; case TEST_METHOD: errors.addAll(checkTestMethod()); break; } if (INSTRUMENTATION_RUNNER_CLASS.length() > 0) { if (facade.findClass(INSTRUMENTATION_RUNNER_CLASS, module.getModuleWithDependenciesAndLibrariesScope(true)) == null) { errors.add(ValidationError .fatal(AndroidBundle.message("instrumentation.runner.class.not.specified.error"))); } } final AndroidFacetConfiguration configuration = facet.getConfiguration(); if (!facet.requiresAndroidModel() && !configuration.getState().PACK_TEST_CODE) { final int count = getTestSourceRootCount(module); if (count > 0) { final String shortMessage = "Test code not included into APK"; final String fixMessage = "Code and resources under test source " + (count > 1 ? "roots" : "root") + " aren't included into debug APK.\nWould you like to include them and recompile " + module.getName() + " module?" + "\n(You may change this option in Android facet settings later)"; Runnable quickFix = new Runnable() { @Override public void run() { final int result = Messages.showYesNoCancelDialog(getProject(), fixMessage, shortMessage, Messages.getQuestionIcon()); if (result == Messages.YES) { configuration.getState().PACK_TEST_CODE = true; } } }; errors.add(ValidationError.fatal(shortMessage, quickFix)); } } return errors; } @Override @NotNull protected ApkProvider getApkProvider(@NotNull AndroidFacet facet, @NotNull ApplicationIdProvider applicationIdProvider) { if (facet.getAndroidModel() != null && facet.getAndroidModel() instanceof AndroidModuleModel) { return new GradleApkProvider(facet, applicationIdProvider, true); } return new NonGradleApkProvider(facet, applicationIdProvider, null); } private static int getTestSourceRootCount(@NotNull Module module) { final ModuleRootManager manager = ModuleRootManager.getInstance(module); return manager.getSourceRoots(true).length - manager.getSourceRoots(false).length; } private List<ValidationError> checkTestMethod() { JavaRunConfigurationModule configurationModule = getConfigurationModule(); final PsiClass testClass; try { testClass = configurationModule.checkModuleAndClassName(CLASS_NAME, ExecutionBundle.message("no.test.class.specified.error.text")); } catch (RuntimeConfigurationException e) { // We can't proceed without a test class. return ImmutableList.of(ValidationError.fromException(e)); } List<ValidationError> errors = Lists.newArrayList(); if (!JUnitUtil.isTestClass(testClass)) { errors.add(ValidationError .warning(ExecutionBundle.message("class.isnt.test.class.error.message", CLASS_NAME))); } if (METHOD_NAME == null || METHOD_NAME.trim().length() == 0) { errors.add(ValidationError.fatal(ExecutionBundle.message("method.name.not.specified.error.message"))); } final JUnitUtil.TestMethodFilter filter = new JUnitUtil.TestMethodFilter(testClass); boolean found = false; boolean testAnnotated = false; for (final PsiMethod method : testClass.findMethodsByName(METHOD_NAME, true)) { if (filter.value(method)) found = true; if (JUnitUtil.isTestAnnotated(method)) testAnnotated = true; } if (!found) { errors.add(ValidationError .warning(ExecutionBundle.message("test.method.doesnt.exist.error.message", METHOD_NAME))); } if (!AnnotationUtil.isAnnotated(testClass, JUnitUtil.RUN_WITH, true) && !testAnnotated) { try { final PsiClass testCaseClass = JUnitUtil.getTestCaseClass(configurationModule.getModule()); if (!testClass.isInheritor(testCaseClass, true)) { errors.add(ValidationError.fatal( ExecutionBundle.message("class.isnt.inheritor.of.testcase.error.message", CLASS_NAME))); } } catch (JUnitUtil.NoJUnitException e) { errors.add(ValidationError .warning(ExecutionBundle.message(AndroidBundle.message("cannot.find.testcase.error")))); } } return errors; } @NotNull @Override public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() { Project project = getProject(); AndroidRunConfigurationEditor<AndroidTestRunConfiguration> editor = new AndroidRunConfigurationEditor<AndroidTestRunConfiguration>( project, new Predicate<AndroidFacet>() { @Override public boolean apply(@Nullable AndroidFacet facet) { return facet != null && supportsRunningLibraryProjects(facet).getFirst(); } }, this); editor.setConfigurationSpecificEditor(new TestRunParameters(project, editor.getModuleSelector())); return editor; } @NotNull @Override protected ConsoleProvider getConsoleProvider() { return new ConsoleProvider() { @NotNull @Override public ConsoleView createAndAttach(@NotNull Disposable parent, @NotNull ProcessHandler handler, @NotNull Executor executor) throws ExecutionException { AndroidTestConsoleProperties properties = new AndroidTestConsoleProperties( AndroidTestRunConfiguration.this, executor); ConsoleView consoleView = SMTestRunnerConnectionUtil.createAndAttachConsole("Android", handler, properties); Disposer.register(parent, consoleView); return consoleView; } }; } @Override protected boolean supportMultipleDevices() { return false; } @Override public boolean monitorRemoteProcess() { // Tests are run using the "am instrument" command. The output from the shell command is processed by AndroidTestListener, // which sends events over to the test UI via GeneralToSMTRunnerEventsConvertor. // If the process handler detects that the test process has terminated before all of the output from that shell process // makes its way through the AndroidTestListener, the test UI marks the test run as having "Terminated" instead of terminating // gracefully once all the test results have been parsed. // As a result, we don't want the process handler monitoring the test process at all in this case.. // See https://code.google.com/p/android/issues/detail?id=201968 return false; } @Nullable @Override protected LaunchTask getApplicationLaunchTask(@NotNull ApplicationIdProvider applicationIdProvider, @NotNull AndroidFacet facet, boolean waitForDebugger, @NotNull LaunchStatus launchStatus) { String runner = StringUtil.isEmpty(INSTRUMENTATION_RUNNER_CLASS) ? findInstrumentationRunner(facet) : INSTRUMENTATION_RUNNER_CLASS; Map<String, String> runnerArguments = getRunnerArguments(facet); String testPackage; try { testPackage = applicationIdProvider.getTestPackageName(); if (testPackage == null) { launchStatus.terminateLaunch("Unable to determine test package name"); return null; } } catch (ApkProvisionException e) { launchStatus.terminateLaunch("Unable to determine test package name"); return null; } return new MyApplicationLaunchTask(runner, testPackage, waitForDebugger, runnerArguments); } @Nullable public static String findInstrumentationRunner(@NotNull AndroidFacet facet) { String runner = getRunnerFromManifest(facet); // TODO: Resolve direct AndroidGradleModel dep (b/22596984) AndroidModuleModel androidModel = AndroidModuleModel.get(facet); if (runner == null && androidModel != null) { Variant selectedVariant = androidModel.getSelectedVariant(); String testRunner = selectedVariant.getMergedFlavor().getTestInstrumentationRunner(); if (testRunner != null) { runner = testRunner; } } return runner; } @NotNull public static Map<String, String> getRunnerArguments(@NotNull AndroidFacet facet) { AndroidModuleModel androidModel = AndroidModuleModel.get(facet); if (androidModel != null) { return new HashMap<>( androidModel.getSelectedVariant().getMergedFlavor().getTestInstrumentationRunnerArguments()); } return Collections.emptyMap(); } @Nullable private static String getRunnerFromManifest(@NotNull final AndroidFacet facet) { if (!ApplicationManager.getApplication().isReadAccessAllowed()) { return ApplicationManager.getApplication().runReadAction(new Computable<String>() { @Override public String compute() { return getRunnerFromManifest(facet); } }); } Manifest manifest = facet.getManifest(); if (manifest != null) { for (Instrumentation instrumentation : manifest.getInstrumentations()) { if (instrumentation != null) { PsiClass instrumentationClass = instrumentation.getInstrumentationClass().getValue(); if (instrumentationClass != null) { return instrumentationClass.getQualifiedName(); } } } } return null; } /** * Returns a refactoring listener that listens to changes in either the package, class or method names * depending on the current {@link #TESTING_TYPE}. */ @Nullable @Override public RefactoringElementListener getRefactoringElementListener(PsiElement element) { if (element instanceof PsiPackage) { String pkgName = ((PsiPackage) element).getQualifiedName(); if (TESTING_TYPE == TEST_ALL_IN_PACKAGE && !StringUtil.equals(pkgName, PACKAGE_NAME)) { // testing package, but the refactored package does not match our package return null; } else if (TESTING_TYPE != TEST_ALL_IN_PACKAGE && !StringUtil.equals(pkgName, StringUtil.getPackageName(CLASS_NAME))) { // testing a class or a method, but the refactored package doesn't match our containing package return null; } return new RefactoringElementAdapter() { @Override protected void elementRenamedOrMoved(@NotNull PsiElement newElement) { if (newElement instanceof PsiPackage) { String newPkgName = ((PsiPackage) newElement).getQualifiedName(); if (TESTING_TYPE == TEST_ALL_IN_PACKAGE) { PACKAGE_NAME = newPkgName; } else { CLASS_NAME = CLASS_NAME.replace(StringUtil.getPackageName(CLASS_NAME), newPkgName); } } } @Override public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) { if (newElement instanceof PsiPackage) { if (TESTING_TYPE == TEST_ALL_IN_PACKAGE) { PACKAGE_NAME = oldQualifiedName; } else { CLASS_NAME = CLASS_NAME.replace(StringUtil.getPackageName(CLASS_NAME), oldQualifiedName); } } } }; } else if ((TESTING_TYPE == TEST_CLASS || TESTING_TYPE == TEST_METHOD) && element instanceof PsiClass) { if (!StringUtil.equals(JavaExecutionUtil.getRuntimeQualifiedName((PsiClass) element), CLASS_NAME)) { return null; } return new RefactoringElementAdapter() { @Override protected void elementRenamedOrMoved(@NotNull PsiElement newElement) { if (newElement instanceof PsiClass) { CLASS_NAME = JavaExecutionUtil.getRuntimeQualifiedName((PsiClass) newElement); } } @Override public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) { if (newElement instanceof PsiClass) { CLASS_NAME = oldQualifiedName; } } }; } else if (TESTING_TYPE == TEST_METHOD && element instanceof PsiMethod) { PsiMethod psiMethod = (PsiMethod) element; if (!StringUtil.equals(psiMethod.getName(), METHOD_NAME)) { return null; } PsiClass psiClass = psiMethod.getContainingClass(); if (psiClass == null) { return null; } String fqName = psiClass.getQualifiedName(); if (fqName != null && !StringUtil.equals(fqName, CLASS_NAME)) { return null; } return new RefactoringElementAdapter() { @Override protected void elementRenamedOrMoved(@NotNull PsiElement newElement) { if (newElement instanceof PsiMethod) { METHOD_NAME = ((PsiMethod) newElement).getName(); } } @Override public void undoElementMovedOrRenamed(@NotNull PsiElement newElement, @NotNull String oldQualifiedName) { if (newElement instanceof PsiMethod) { METHOD_NAME = oldQualifiedName; } } }; } return null; } private class MyApplicationLaunchTask implements LaunchTask { @Nullable private final String myInstrumentationTestRunner; @NotNull private final String myTestApplicationId; private final boolean myWaitForDebugger; @NotNull private final Map<String, String> myInstrumentationTestRunnerArguments; public MyApplicationLaunchTask(@Nullable String runner, @NotNull String testPackage, boolean waitForDebugger, @NotNull Map<String, String> arguments) { myInstrumentationTestRunner = runner; myWaitForDebugger = waitForDebugger; myTestApplicationId = testPackage; myInstrumentationTestRunnerArguments = arguments; } @NotNull @Override public String getDescription() { return "Launching instrumentation runner"; } @Override public int getDuration() { return 2; } @Override public boolean perform(@NotNull IDevice device, @NotNull final LaunchStatus launchStatus, @NotNull final ConsolePrinter printer) { printer.stdout("Running tests\n"); final RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(myTestApplicationId, myInstrumentationTestRunner, device); switch (TESTING_TYPE) { case TEST_ALL_IN_PACKAGE: runner.setTestPackageName(PACKAGE_NAME); break; case TEST_CLASS: runner.setClassName(CLASS_NAME); break; case TEST_METHOD: runner.setMethodName(CLASS_NAME, METHOD_NAME); break; } runner.setDebug(myWaitForDebugger); runner.setRunOptions(EXTRA_OPTIONS); for (Map.Entry<String, String> entry : myInstrumentationTestRunnerArguments.entrySet()) { runner.addInstrumentationArg(entry.getKey(), entry.getValue()); } printer.stdout("$ adb shell " + runner.getAmInstrumentCommand()); // run in a separate thread as this will block until the tests complete ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { @Override public void run() { try { runner.run(new AndroidTestListener(launchStatus, printer)); } catch (Exception e) { LOG.info(e); printer.stderr("Error: Unexpected exception while running tests: " + e); } } }); return true; } } }