/* * Copyright (C) 2014 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.build.gradle.integration.common.fixture; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.build.gradle.integration.common.fixture.app.AbstractAndroidTestApp; import com.android.build.gradle.integration.common.fixture.app.AndroidTestApp; import com.android.build.gradle.integration.common.fixture.app.TestSourceFile; import com.android.build.gradle.integration.common.utils.FileHelper; import com.android.build.gradle.integration.common.utils.JacocoAgent; import com.android.build.gradle.integration.common.utils.SdkHelper; import com.android.builder.core.BuilderConstants; import com.android.builder.model.AndroidProject; import com.android.builder.model.SyncIssue; import com.android.builder.model.Version; import com.android.io.StreamException; import com.android.sdklib.internal.project.ProjectProperties; import com.android.sdklib.internal.project.ProjectPropertiesWorkingCopy; import com.android.sdklib.repository.FullRevision; import com.google.common.base.Charsets; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.io.Files; import org.gradle.tooling.BuildAction; import org.gradle.tooling.BuildActionExecuter; import org.gradle.tooling.BuildLauncher; import org.gradle.tooling.GradleConnectionException; import org.gradle.tooling.GradleConnector; import org.gradle.tooling.LongRunningOperation; import org.gradle.tooling.ProjectConnection; import org.gradle.tooling.ResultHandler; import org.gradle.tooling.internal.consumer.DefaultGradleConnector; import org.gradle.tooling.model.GradleProject; import org.gradle.tooling.model.GradleTask; import org.junit.Assume; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.Charset; import java.security.CodeSource; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; /** * JUnit4 test rule for integration test. * * This rule create a gradle project in a temporary directory. * It can be use with the @Rule or @ClassRule annotations. Using this class with @Rule will create * a gradle project in separate directories for each unit test, whereas using it with @ClassRule * creates a single gradle project. * * The test directory is always deleted if it already exists at the start of the test to ensure a * clean environment. */ public class GradleTestProject implements TestRule { public static final File TEST_RES_DIR = new File("src/test/resources"); public static final int DEFAULT_COMPILE_SDK_VERSION = 21; public static final String DEFAULT_BUILD_TOOL_VERSION; public static final String REMOTE_TEST_PROVIDER = System.getenv().get("REMOTE_TEST_PROVIDER"); public static final String DEVICE_PROVIDER_NAME = REMOTE_TEST_PROVIDER != null ? REMOTE_TEST_PROVIDER : BuilderConstants.CONNECTED; public static final String GRADLE_TEST_VERSION = "2.2.1"; public static final String GRADLE_EXP_TEST_VERSION = "2.5"; public static final String ANDROID_GRADLE_PLUGIN_VERSION; public static final String CUSTOM_JACK; public static final boolean USE_JACK; private static final String RECORD_BENCHMARK_NAME = "com.android.benchmark.name"; private static final String RECORD_BENCHMARK_MODE = "com.android.benchmark.mode"; public enum BenchmarkMode { EVALUATION, SYNC, BUILD_FULL, BUILD_INC_JAVA, BUILD_INC_RES_EDIT, BUILD_INC_RES_ADD } static { String envBuildToolVersion = System.getenv("CUSTOM_BUILDTOOLS"); DEFAULT_BUILD_TOOL_VERSION = !Strings.isNullOrEmpty(envBuildToolVersion) ? envBuildToolVersion : "22.0.1"; String envVersion = System.getenv().get("CUSTOM_GRADLE"); ANDROID_GRADLE_PLUGIN_VERSION = !Strings.isNullOrEmpty(envVersion) ? envVersion : Version.ANDROID_GRADLE_PLUGIN_VERSION; String envJack = System.getenv().get("CUSTOM_JACK"); CUSTOM_JACK = !Strings.isNullOrEmpty(envJack) ? envJack : "false"; USE_JACK = Boolean.parseBoolean(CUSTOM_JACK); } private static final String COMMON_HEADER = "commonHeader.gradle"; private static final String COMMON_LOCAL_REPO = "commonLocalRepo.gradle"; private static final String COMMON_BUILD_SCRIPT = "commonBuildScript.gradle"; private static final String COMMON_BUILD_SCRIPT_EXP = "commonBuildScriptExperimental.gradle"; private static final String COMMON_GRADLE_PLUGIN_VERSION = "commonGradlePluginVersion.gradle"; private static final String DEFAULT_TEST_PROJECT_NAME = "project"; public static class Builder { private static final File SAMPLE_PROJECT_DIR = new File("samples"); private static final File TEST_PROJECT_DIR = new File("test-projects"); @Nullable private String name; @Nullable private TestProject testProject = null; @Nullable File sdkDir = SdkHelper.findSdkDir(); @Nullable File ndkDir = findNdkDir(); boolean captureStdOut = false; boolean captureStdErr = false; boolean experimentalMode = false; @Nullable private String targetGradleVersion; boolean useJack = false; boolean useMinify = false; @NonNull private List<String> gradleProperties = Lists.newArrayList(); @Nullable private String heapSize; /** * Create a GradleTestProject. */ public GradleTestProject create() { if (targetGradleVersion == null) { targetGradleVersion = experimentalMode ? GRADLE_EXP_TEST_VERSION : GRADLE_TEST_VERSION; } return new GradleTestProject( name, testProject, experimentalMode, useMinify, useJack, targetGradleVersion, captureStdOut, captureStdErr, sdkDir, ndkDir, gradleProperties, heapSize); } /** * Set the name of the project. * * Necessary if you have multiple projects in a test class. */ public Builder withName(@NonNull String name) { this.name = name; return this; } public Builder captureStdOut(boolean captureStdOut) { this.captureStdOut = captureStdOut; return this; } public Builder captureStdErr(boolean captureStdErr) { this.captureStdErr = captureStdErr; return this; } /** * Use experimental plugin for the test project. */ public Builder forExpermimentalPlugin(boolean mode) { this.experimentalMode = mode; return this; } /** * Use the gradle version for experimental plugin, but the test project do not necessarily * have to use experimental plugin. */ public Builder useExperimentalGradleVersion(boolean mode) { if (mode) { targetGradleVersion = GRADLE_EXP_TEST_VERSION; } return this; } /** * Use the gradle version specified, e.g. "2.4". */ public Builder useGradleVersion(String targetGradleVersion) { this.targetGradleVersion = targetGradleVersion; return this; } /** * Create a project without setting ndk.dir in local.properties. */ public Builder withoutNdk() { this.ndkDir = null; return this; } /** * Create GradleTestProject from a TestProject. */ public Builder fromTestApp(@NonNull TestProject testProject) { this.testProject = testProject; return this; } /** * Create GradleTestProject from an existing test project. */ public Builder fromTestProject(@NonNull String project) { AndroidTestApp app = new EmptyTestApp(); name = project; File projectDir = new File(TEST_PROJECT_DIR, project); addAllFiles(app, projectDir); return fromTestApp(app); } /** * Create GradleTestProject from an existing test project. */ public Builder fromExternalProject(@NonNull String project) throws IOException { AndroidTestApp app = new EmptyTestApp(); name = project; // compute the root folder of the checkout, based on test-projects. File parentDir = TEST_PROJECT_DIR.getCanonicalFile().getParentFile().getParentFile() .getParentFile().getParentFile().getParentFile(); parentDir = new File(parentDir, "external"); File projectDir = new File(parentDir, project); addAllFiles(app, projectDir); return fromTestApp(app); } /** * Add gradle properties. */ public Builder addGradleProperties(@NonNull String property) { gradleProperties.add(property); return this; } /** * Sets the test heap size requirement. Example values : 1024m, 2048m... * * @param heapSize the heap size in a format understood by the -Xmx JVM parameter * @return itself. */ public Builder withHeap(String heapSize) { this.heapSize = heapSize; return this; } public Builder withJack(boolean useJack) { this.useJack = useJack; return this; } public Builder withMinify(boolean useMinify) { this.useMinify = useMinify; return this; } private static class EmptyTestApp extends AbstractAndroidTestApp { @Override public boolean containsFullBuildScript() { return true; } } } private final String name; private final File outDir; private File testDir; private File sourceDir; private File buildFile; private final File ndkDir; private final File sdkDir; private final ByteArrayOutputStream stdout; private final ByteArrayOutputStream stderr; private final Collection<String> gradleProperties; @Nullable private final TestProject testProject; private final boolean experimentalMode; private final String targetGradleVersion; private final boolean useJack; private final boolean minifyEnabled; @Nullable private String heapSize; private GradleTestProject( @Nullable String name, @Nullable TestProject testProject, boolean experimentalMode, boolean minifyEnabled, boolean useJack, String targetGradleVersion, boolean captureStdOut, boolean captureStdErr, @Nullable File sdkDir, @Nullable File ndkDir, @NonNull Collection<String> gradleProperties, @Nullable String heapSize) { String buildDir = System.getenv("PROJECT_BUILD_DIR"); outDir = (buildDir == null) ? new File("build/tests") : new File(buildDir, "tests"); testDir = null; buildFile = sourceDir = null; this.name = (name == null) ? DEFAULT_TEST_PROJECT_NAME : name; this.experimentalMode = experimentalMode; this.minifyEnabled = minifyEnabled; this.useJack = useJack; this.targetGradleVersion = targetGradleVersion; this.testProject = testProject; stdout = captureStdOut ? new ByteArrayOutputStream() : null; stderr = captureStdErr ? new ByteArrayOutputStream() : null; this.sdkDir = sdkDir; this.ndkDir = ndkDir; this.heapSize = heapSize; this.gradleProperties = gradleProperties; } /** * Create a GradleTestProject representing a subProject of another GradleTestProject. * @param subProject name of the subProject. * @param rootProject root GradleTestProject. */ private GradleTestProject( @NonNull String subProject, @NonNull GradleTestProject rootProject) { name = subProject; outDir = rootProject.outDir; testDir = new File(rootProject.testDir, subProject); assertTrue("No subproject dir at " + testDir.toString(), testDir.isDirectory()); buildFile = new File(testDir, "build.gradle"); sourceDir = new File(testDir, "src"); ndkDir = rootProject.ndkDir; sdkDir = rootProject.sdkDir; stdout = rootProject.stdout; stderr = rootProject.stdout; gradleProperties = ImmutableList.of(); testProject = null; experimentalMode = rootProject.isExperimentalMode(); targetGradleVersion = rootProject.getTargetGradleVersion(); minifyEnabled = false; useJack = false; } String getTargetGradleVersion() { return targetGradleVersion; } boolean isExperimentalMode() { return experimentalMode; } public static Builder builder() { return new Builder(); } /** * Recursively delete directory or file. * * @param root directory to delete */ private static void deleteRecursive(File root) { if (root.exists()) { if (root.isDirectory()) { File files[] = root.listFiles(); if (files != null) { for (File file : files) { deleteRecursive(file); } } } assertTrue(root.delete()); } } /** * Add all files in a directory to an AndroidTestApp. */ private static void addAllFiles(AndroidTestApp app, File projectDir) { for (String filePath : FileHelper.listFiles(projectDir)) { File file = new File(filePath); try { app.addFile( new TestSourceFile( file.getParent(), file.getName(), Files.toByteArray(new File(projectDir, filePath)))); } catch (IOException e) { fail(e.toString()); } } } @Override public Statement apply(final Statement base, Description description) { testDir = new File(outDir, description.getTestClass().getName()); // Create separate directory based on test method name if @Rule is used. // getMethodName() is null if this rule is used as a @ClassRule. if (description.getMethodName() != null) { String dirName = description.getMethodName(); dirName = dirName.replaceAll("[^a-zA-Z0-9_]", "_"); testDir = new File(testDir, dirName); } testDir = new File(testDir, name); buildFile = new File(testDir, "build.gradle"); sourceDir = new File(testDir, "src"); return new Statement() { @Override public void evaluate() throws Throwable { if (testDir.exists()) { deleteRecursive(testDir); } assertTrue(testDir.mkdirs()); assertTrue(sourceDir.mkdirs()); Files.copy( new File(Builder.TEST_PROJECT_DIR, COMMON_HEADER), new File(testDir.getParent(), COMMON_HEADER)); Files.copy( new File(Builder.TEST_PROJECT_DIR, COMMON_LOCAL_REPO), new File(testDir.getParent(), COMMON_LOCAL_REPO)); Files.copy( new File(Builder.TEST_PROJECT_DIR, COMMON_BUILD_SCRIPT), new File(testDir.getParent(), COMMON_BUILD_SCRIPT)); Files.copy( new File(Builder.TEST_PROJECT_DIR, COMMON_BUILD_SCRIPT_EXP), new File(testDir.getParent(), COMMON_BUILD_SCRIPT_EXP)); Files.copy( new File(Builder.TEST_PROJECT_DIR, COMMON_GRADLE_PLUGIN_VERSION), new File(testDir.getParent(), COMMON_GRADLE_PLUGIN_VERSION)); if (testProject != null) { testProject.write( testDir, testProject.containsFullBuildScript() ? "" :getGradleBuildscript()); } else { Files.write( getGradleBuildscript(), buildFile, Charsets.UTF_8); } createLocalProp(testDir, sdkDir, ndkDir); createGradleProp(); base.evaluate(); } }; } /** * Create a GradleTestProject representing a subproject. */ public GradleTestProject getSubproject(String name) { return new GradleTestProject(name, this); } /** * Return the name of the test project. */ public String getName() { return name; } /** * Return the directory containing the test project. */ public File getTestDir() { return testDir; } /** * Return the build.gradle of the test project. */ public File getBuildFile() { return buildFile; } /** * Return the directory containing the source files of the test project. */ public File getSourceDir() { return sourceDir; } /** * Return the output directory from Android plugins. */ public File getOutputDir() { return new File(testDir, Joiner.on(File.separator).join("build", AndroidProject.FD_OUTPUTS)); } /** * Return a File under the output directory from Android plugins. */ public File getOutputFile(String path) { return new File(getOutputDir(), path); } /** * Return the output apk File from the application plugin for the given dimension. * * Expected dimensions orders are: * - product flavors * - build type * - other modifiers (e.g. "unsigned", "aligned") */ public File getApk(String ... dimensions) { // TODO: Add overload for tests and splits. List<String> dimensionList = Lists.newArrayListWithExpectedSize(1 + dimensions.length); dimensionList.add(getName()); dimensionList.addAll(Arrays.asList(dimensions)); return getOutputFile( "apk/" + Joiner.on("-").join(dimensionList) + SdkConstants.DOT_ANDROID_PACKAGE); } /** * Return the output aar File from the library plugin for the given dimension. * * Expected dimensions orders are: * - product flavors * - build type * - other modifiers (e.g. "unsigned", "aligned") */ public File getAar(String ... dimensions) { // TODO: Add overload for tests and splits. List<String> dimensionList = Lists.newArrayListWithExpectedSize(1 + dimensions.length); dimensionList.add(getName()); dimensionList.addAll(Arrays.asList(dimensions)); return getOutputFile("aar/" + Joiner.on("-").join(dimensionList) + SdkConstants.DOT_AAR); } /** * Returns the SDK dir */ public File getSdkDir() { return sdkDir; } /** * Returns the NDK dir */ public File getNdkDir() { return ndkDir; } /** * Return the directory of the repository containing the necessary plugins for testing. */ private File getRepoDir() { CodeSource source = getClass().getProtectionDomain().getCodeSource(); assert (source != null); URL location = source.getLocation(); try { File dir = new File(location.toURI()); assertTrue(dir.getPath(), dir.exists()); File f = dir.getParentFile().getParentFile().getParentFile().getParentFile() .getParentFile().getParentFile().getParentFile(); return new File(f, "out" + File.separator + "repo"); } catch (URISyntaxException e) { fail(e.getLocalizedMessage()); } return null; } /** * Returns a string that contains the gradle buildscript content */ public String getGradleBuildscript() { return "apply from: \"../commonHeader.gradle\"\n" + "buildscript { apply from: \"../commonBuildScript" + (experimentalMode ? "Experimental" : "") + ".gradle\", to: buildscript }\n" + "\n" + "apply from: \"../commonLocalRepo.gradle\"\n"; } /** * Return a list of all task names of the project. */ public List<String> getTaskList() { ProjectConnection connection = getProjectConnection(); try { GradleProject project = connection.getModel(GradleProject.class); List<String> tasks = Lists.newArrayList(); for (GradleTask gradleTask : project.getTasks()) { tasks.add(gradleTask.getName()); } return tasks; } finally { connection.close(); } } /** * Runs gradle on the project. Throws exception on failure. * * @param tasks Variadic list of tasks to execute. */ public void execute(String ... tasks) { execute(Collections.<String>emptyList(), false, false, ExpectedBuildResult.SUCCESS, tasks); } public void execute(@NonNull List<String> arguments, String ... tasks) { execute(arguments, false, false, ExpectedBuildResult.SUCCESS, tasks); } public void executeWithBenchmark( @NonNull String benchmarkName, @NonNull BenchmarkMode benchmarkMode, String ... tasks) { List<String> arguments = ImmutableList.of( "-P" + RECORD_BENCHMARK_NAME + "=" + benchmarkName, "-P" + RECORD_BENCHMARK_MODE + "=" + benchmarkMode.name().toLowerCase(Locale.US) ); execute(arguments, false, false, ExpectedBuildResult.SUCCESS, tasks); } public void executeExpectingFailure(String... tasks) { executeExpectingFailure(Collections.<String>emptyList(), tasks); } public void executeExpectingFailure(@NonNull List<String> arguments, String... tasks) { execute( arguments, false /*returnModel*/, false /*emulateStudio_1_0*/, ExpectedBuildResult.FAILURE, tasks); } public void executeConnectedCheck() { executeConnectedCheck(Collections.<String>emptyList()); } public void executeConnectedCheck(List<String> arguments) { execute(arguments, REMOTE_TEST_PROVIDER == null ? "connectedCheck" : "deviceCheck"); } /** * Runs gradle on the project, and returns the project model. Throws exception on failure. * * @param tasks Variadic list of tasks to execute. * * @return the AndroidProject model for the project. */ @NonNull public AndroidProject executeAndReturnModel(String ... tasks) { return executeAndReturnModel(false, tasks); } /** * Runs gradle on the project, and returns the project model. Throws exception on failure. * * @param emulateStudio_1_0 whether to emulate an older IDE (studio 1.0) querying the model. * @param tasks Variadic list of tasks to execute. * * @return the AndroidProject model for the project. */ @NonNull public AndroidProject executeAndReturnModel(boolean emulateStudio_1_0, String ... tasks) { //noinspection ConstantConditions return execute(Collections.<String>emptyList(), true, emulateStudio_1_0, ExpectedBuildResult.SUCCESS, tasks); } /** * Runs gradle on the project, and returns a project model for each sub-project. * Throws exception on failure. * * @param tasks Variadic list of tasks to execute. * * @return the AndroidProject model for the project. */ @NonNull public Map<String, AndroidProject> executeAndReturnMultiModel(String ... tasks) { return executeAndReturnMultiModel(false, tasks); } /** * Runs gradle on the project, and returns a project model for each sub-project. * Throws exception on failure. * * @param emulateStudio_1_0 whether to emulate an older IDE (studio 1.0) querying the model. * @param tasks Variadic list of tasks to execute. * * @return the AndroidProject model for the project. */ @NonNull public Map<String, AndroidProject> executeAndReturnMultiModel(boolean emulateStudio_1_0, String ... tasks) { ProjectConnection connection = getProjectConnection(); try { executeBuild(Collections.<String>emptyList(), connection, tasks, ExpectedBuildResult.SUCCESS); return buildModel(connection, new GetAndroidModelAction(), emulateStudio_1_0, null, null); } finally { connection.close(); } } /** * Returns the project model without building. * * This will fail if the project is a multi-project setup or if there are any sync issues * while loading the project. */ @NonNull public AndroidProject getSingleModel() { return getSingleModel(false /* emulateStudio_1_0 */, true /*assertNodSyncIssues */); } /** * Returns the project model without building, querying it the way Studio 1.0 does. * * This will fail if the project is a multi-project setup or if there are any sync issues * while loading the project. */ @NonNull public AndroidProject getSingleModelAsStudio1() { return getSingleModel(true /* emulateStudio_1_0 */, true /*assertNodSyncIssues */); } /** * Returns the project model without building. * * This will fail if the project is a multi-project setup. */ @NonNull public AndroidProject getSingleModelIgnoringSyncIssues() { return getSingleModel(false /* emulateStudio_1_0 */, false /*assertNodSyncIssues */); } /** * Returns the project model without building, querying it the way Studio 1.0 does. * * This will fail if the project is a multi-project setup. */ @NonNull public AndroidProject getSingleModelIgnoringSyncIssuesAsStudio1() { return getSingleModel(true /* emulateStudio_1_0 */, false /*assertNodSyncIssues */); } /** * Returns the project model without building. * * This will fail if the project is a multi-project setup. * * @param emulateStudio_1_0 whether to emulate an older IDE (studio 1.0) querying the model. * @param assertNoSyncIssues true if the presence of sync issues during the model evaluation * should raise a {@link AssertionError}s */ @NonNull private AndroidProject getSingleModel(boolean emulateStudio_1_0, boolean assertNoSyncIssues) { ProjectConnection connection = getProjectConnection(); try { Map<String, AndroidProject> modelMap = buildModel( connection, new GetAndroidModelAction(), emulateStudio_1_0, null, null); // ensure there was only one project assertEquals("Quering GradleTestProject.getModel() with multi-project settings", 1, modelMap.size()); AndroidProject androidProject = modelMap.get(":"); if (assertNoSyncIssues) { assertNoSyncIssues(androidProject); } return androidProject; } finally { connection.close(); } } /** * Returns a project model for each sub-project without building generating a * {@link AssertionError} if any sync issue is raised during the model loading. */ @NonNull public Map<String, AndroidProject> getAllModels() { return getAllModelsWithBenchmark(null, null); } /** * Returns a project model for each sub-project without building generating a * @param benchmarkName optional benchmark name to pass to Gradle * @param benchmarkMode optional benchmark mode to pass to gradle. * {@link AssertionError} if any sync issue is raised during the model loading. */ @NonNull public Map<String, AndroidProject> getAllModelsWithBenchmark( @Nullable String benchmarkName, @Nullable BenchmarkMode benchmarkMode) { Map<String, AndroidProject> allModels = getAllModels(new GetAndroidModelAction(), false, benchmarkName, benchmarkMode); for (AndroidProject project : allModels.values()) { assertNoSyncIssues(project); } return allModels; } private static void assertNoSyncIssues(AndroidProject project) { if (!project.getSyncIssues().isEmpty()) { StringBuilder msg = new StringBuilder(); msg.append("Project ") .append(project.getName()) .append(" had sync issues :\n"); for (SyncIssue syncIssue : project.getSyncIssues()) { msg.append(syncIssue); msg.append("\n"); } fail(msg.toString()); } } /** * Returns a project model for each sub-project without building and ignoring potential sync * issues. Sync issues will still be returned for each {@link AndroidProject} that failed to * sync properly. */ @NonNull public Map<String, AndroidProject> getAllModelsIgnoringSyncIssues() { return getAllModels(new GetAndroidModelAction(), false, null, null); } /** * Returns a project model for each sub-project without building. * * @param action the build action to gather the model */ @NonNull public <K, V> Map<K, V> getAllModels(@NonNull BuildAction<Map<K, V>> action) { return getAllModels(action, false, null, null); } /** * Returns a project model for each sub-project without building. * * @param action the build action to gather the model * @param emulateStudio_1_0 whether to emulate an older IDE (studio 1.0) querying the model. * @param benchmarkName optional benchmark name to pass to Gradle * @param benchmarkMode optional benchmark mode to pass to gradle. */ @NonNull public <K, V> Map<K, V> getAllModels( @NonNull BuildAction<Map<K, V>> action, boolean emulateStudio_1_0, @Nullable String benchmarkName, @Nullable BenchmarkMode benchmarkMode) { ProjectConnection connection = getProjectConnection(); try { return buildModel(connection, action, emulateStudio_1_0, benchmarkName, benchmarkMode); } finally { connection.close(); } } /** * Runs gradle on the project. Throws exception on failure. * * @param arguments List of arguments for the gradle command. * @param returnModel whether the model should be queried and returned. * @param emulateStudio_1_0 whether to emulate an older IDE (studio 1.0) querying the model. * @param expectedBuildResult the expected result. If the build status does not match the * expected failure, then an exception will be thrown. * @param tasks Variadic list of tasks to execute. * * @return the model, if <var>returnModel</var> was true, null otherwise */ @Nullable private AndroidProject execute( @NonNull List<String> arguments, boolean returnModel, boolean emulateStudio_1_0, ExpectedBuildResult expectedBuildResult, @NonNull String ... tasks) { ProjectConnection connection = getProjectConnection(); try { executeBuild(arguments, connection, tasks, expectedBuildResult); if (returnModel) { Map<String, AndroidProject> modelMap = buildModel( connection, new GetAndroidModelAction(), emulateStudio_1_0, null, null); // ensure there was only one project assertEquals("Quering GradleTestProject.getModel() with multi-project settings", 1, modelMap.size()); return modelMap.get(":"); } } finally { connection.close(); } return null; } private enum ExpectedBuildResult { SUCCESS, FAILURE } private void executeBuild(final List<String> arguments, ProjectConnection connection, final String[] tasks, ExpectedBuildResult expectedBuildResult) { List<String> args = Lists.newArrayListWithCapacity(5 + arguments.size()); args.add("-i"); args.add("-u"); args.add("-Pcom.android.build.gradle.integratonTest.useJack=" + Boolean.toString(useJack)); args.add("-Pcom.android.build.gradle.integratonTest.minifyEnabled=" + Boolean.toString(minifyEnabled)); args.add("-Pcom.android.build.gradle.integratonTest.useComponentModel=" + Boolean.toString(experimentalMode)); args.addAll(arguments); BuildLauncher launcher = connection.newBuild() .forTasks(tasks) .withArguments(Iterables.toArray(args, String.class)); setJvmArguments(launcher); if (stdout != null) { launcher.setStandardOutput(stdout); } else { launcher.setStandardOutput(System.out); } if (stderr != null) { launcher.setStandardError(stderr); } else { launcher.setStandardError(System.err); } if (expectedBuildResult == ExpectedBuildResult.SUCCESS) { launcher.run(); } else { launcher.run(new ResultHandler<Void>() { @Override public void onComplete(Void aVoid) { throw new AssertionError( String.format( "Expecting build to fail:\n" + " Tasks: %s\n" + " Arguments: %s", Joiner.on(' ').join(tasks), Joiner.on(' ').join(arguments))); } @Override public void onFailure(GradleConnectionException e) { // Ignore, the test expects this build to fail. } }); } } private void setJvmArguments(LongRunningOperation launcher) { List<String> jvmArguments = new ArrayList<String>(); if (!Strings.isNullOrEmpty(heapSize)) { jvmArguments.add("-Xmx" + heapSize); } jvmArguments.add("-XX:MaxPermSize=1024m"); String debugIntegrationTest = System.getenv("DEBUG_INNER_TEST"); if (!Strings.isNullOrEmpty(debugIntegrationTest)) { jvmArguments.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5006"); } if (JacocoAgent.isJacocoEnabled()) { jvmArguments.add(JacocoAgent.getJvmArg()); } launcher.setJvmArguments(Iterables.toArray(jvmArguments, String.class)); } /** * Returns a project model for each sub-project without building. * @param connection the opened ProjectConnection * @param action the build action to gather the model * @param emulateStudio_1_0 whether to emulate an older IDE (studio 1.0) querying the model. * @param benchmarkName optional benchmark name to pass to Gradle * @param benchmarkMode optional benchmark mode to pass to gradle. */ @NonNull private <K,V> Map<K, V> buildModel( @NonNull ProjectConnection connection, @NonNull BuildAction<Map<K, V>> action, boolean emulateStudio_1_0, @Nullable String benchmarkName, @Nullable BenchmarkMode benchmarkMode) { BuildActionExecuter<Map<K, V>> executor = connection.action(action); List<String> arguments = Lists.newArrayListWithCapacity(5); arguments.add("-P" + AndroidProject.PROPERTY_BUILD_MODEL_ONLY + "=true"); arguments.add("-P" + AndroidProject.PROPERTY_INVOKED_FROM_IDE + "=true"); if (!emulateStudio_1_0) { arguments.add("-P" + AndroidProject.PROPERTY_BUILD_MODEL_ONLY_ADVANCED + "=true"); } if (benchmarkName != null) { arguments.add("-P" + RECORD_BENCHMARK_NAME + "=" + benchmarkName); if (benchmarkMode != null) { arguments.add("-P" + RECORD_BENCHMARK_MODE + "=" + benchmarkMode.name().toLowerCase(Locale.US)); } } setJvmArguments(executor); executor.withArguments(Iterables.toArray(arguments, String.class)); executor.setStandardOutput(System.out); executor.setStandardError(System.err); return executor.run(); } /** * Return the stdout from all execute command. */ public ByteArrayOutputStream getStdout() { return stdout; } /** * Return the stderr from all execute command. */ public ByteArrayOutputStream getStderr() { return stderr; } /** * Create a File object. getTestDir will be the base directory if a relative path is supplied. * * @param path Full path of the file. May be a relative path. */ public File file(String path) { File result = new File(path); if (result.isAbsolute()) { return result; } else { return new File(testDir, path); } } /** * Returns the NDK folder as built from the Android source tree. */ private static File findNdkDir() { String androidHome = System.getenv("ANDROID_NDK_HOME"); if (androidHome != null) { File f = new File(androidHome); if (f.isDirectory()) { return f; } else { System.out.println("Failed to find NDK in ANDROID_NDK_HOME=" + androidHome); } } return null; } /** * Returns a Gradle project Connection */ @NonNull private ProjectConnection getProjectConnection() { GradleConnector connector = GradleConnector.newConnector(); // Limit daemon idle time for tests. 10 seconds is enough for another test // to start and reuse the daemon. ((DefaultGradleConnector) connector).daemonMaxIdleTime(10, TimeUnit.SECONDS); return connector .useGradleVersion(targetGradleVersion) .forProjectDirectory(testDir) .connect(); } private static File createLocalProp( @NonNull File project, @NonNull File sdkDir, @Nullable File ndkDir) throws IOException, StreamException { ProjectPropertiesWorkingCopy localProp = ProjectProperties.create( project.getAbsolutePath(), ProjectProperties.PropertyType.LOCAL); localProp.setProperty(ProjectProperties.PROPERTY_SDK, sdkDir.getAbsolutePath()); if (ndkDir != null) { localProp.setProperty(ProjectProperties.PROPERTY_NDK, ndkDir.getAbsolutePath()); } localProp.save(); return (File) localProp.getFile(); } private void createGradleProp() throws IOException { if (gradleProperties.isEmpty()) { return; } File propertyFile = file("gradle.properties"); Files.write(Joiner.on('\n').join(gradleProperties), propertyFile, Charset.defaultCharset()); } public static void assumeLocalDevice() { Assume.assumeTrue( "Install task not run against device provider", GradleTestProject.REMOTE_TEST_PROVIDER == null); } public static void assumeBuildToolsAtLeast(int major) { assumeBuildToolsAtLeast( major, FullRevision.IMPLICIT_MINOR_REV, FullRevision.IMPLICIT_MICRO_REV); } public static void assumeBuildToolsAtLeast(int major, int minor, int micro) { FullRevision currentVersion = FullRevision.parseRevision(DEFAULT_BUILD_TOOL_VERSION); Assume.assumeTrue("Test is only applicable to build tools > " + major, new FullRevision(major, minor, micro).compareTo(currentVersion) < 0); } /** * Replace a line from a file with another line. * @param relativePath the relative path of the file from the root of the project * @param lineNumber the line number, starting at 1 * @param line the line to replace with. * @throws IOException */ public void replaceLine( String relativePath, int lineNumber, String line) throws IOException { File file = new File(testDir, relativePath.replace("/", File.separator)); List<String> lines = Files.readLines(file, Charsets.UTF_8); lines.add(lineNumber, line); lines.remove(lineNumber - 1); Files.write( Joiner.on(System.getProperty("line.separator")).join(lines), file, Charsets.UTF_8); } }