| // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| package com.android.tools.r8; | |
| import static com.android.tools.r8.ToolHelper.TestDataSourceSet.computeLegacyOrGradleSpecifiedLocation; | |
| import static com.android.tools.r8.utils.internal.FileUtils.CLASS_EXTENSION; | |
| import static com.android.tools.r8.utils.internal.FileUtils.JAVA_EXTENSION; | |
| import static com.android.tools.r8.utils.internal.FileUtils.isDexFile; | |
| import static org.junit.Assert.assertEquals; | |
| import static org.junit.Assert.assertTrue; | |
| import static org.junit.Assert.fail; | |
| import com.android.tools.r8.DeviceRunner.DeviceRunnerConfigurationException; | |
| import com.android.tools.r8.ResourceShrinker.ReferenceChecker; | |
| import com.android.tools.r8.TestBase.Backend; | |
| import com.android.tools.r8.TestRuntime.CfRuntime; | |
| import com.android.tools.r8.TestRuntime.CfVm; | |
| import com.android.tools.r8.ToolHelper.DexVm.Kind; | |
| import com.android.tools.r8.benchmarks.BenchmarkResults; | |
| import com.android.tools.r8.benchmarks.gc.CaptureGcResult; | |
| import com.android.tools.r8.cf.CfVersion; | |
| import com.android.tools.r8.dex.ApplicationReader; | |
| import com.android.tools.r8.dex.Marker.Tool; | |
| import com.android.tools.r8.graph.AppView; | |
| import com.android.tools.r8.graph.AssemblyWriter; | |
| import com.android.tools.r8.graph.DexApplication; | |
| import com.android.tools.r8.graph.DexItemFactory; | |
| import com.android.tools.r8.graph.DexProgramClass; | |
| import com.android.tools.r8.graph.DirectMappedDexApplication; | |
| import com.android.tools.r8.graph.LazyLoadedDexApplication; | |
| import com.android.tools.r8.origin.Origin; | |
| import com.android.tools.r8.position.Position; | |
| import com.android.tools.r8.shaking.FilteredClassPath; | |
| import com.android.tools.r8.shaking.ProguardConfiguration; | |
| import com.android.tools.r8.shaking.ProguardConfigurationParser; | |
| import com.android.tools.r8.shaking.ProguardConfigurationRule; | |
| import com.android.tools.r8.utils.AndroidApiLevel; | |
| import com.android.tools.r8.utils.AndroidApp; | |
| import com.android.tools.r8.utils.AndroidAppConsumers; | |
| import com.android.tools.r8.utils.DexVersion; | |
| import com.android.tools.r8.utils.ExceptionUtils; | |
| import com.android.tools.r8.utils.InternalOptions; | |
| import com.android.tools.r8.utils.Reporter; | |
| import com.android.tools.r8.utils.UncheckedApiLevel; | |
| import com.android.tools.r8.utils.ZipUtils; | |
| import com.android.tools.r8.utils.internal.FileUtils; | |
| import com.android.tools.r8.utils.internal.ListUtils; | |
| import com.android.tools.r8.utils.internal.StringUtils; | |
| import com.android.tools.r8.utils.internal.collections.Pair; | |
| import com.android.tools.r8.utils.internal.exceptions.Unimplemented; | |
| import com.android.tools.r8.utils.internal.exceptions.Unreachable; | |
| import com.android.tools.r8.utils.timing.Timing; | |
| import com.google.common.base.Charsets; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.common.collect.Lists; | |
| import com.google.common.hash.Hasher; | |
| import com.google.common.hash.Hashing; | |
| import com.google.common.io.ByteStreams; | |
| import com.google.gson.Gson; | |
| import java.io.File; | |
| import java.io.FileReader; | |
| import java.io.FileWriter; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.InputStreamReader; | |
| import java.io.PrintStream; | |
| import java.io.Reader; | |
| import java.io.UncheckedIOException; | |
| import java.lang.reflect.Field; | |
| import java.nio.charset.StandardCharsets; | |
| import java.nio.file.FileVisitResult; | |
| import java.nio.file.Files; | |
| import java.nio.file.NoSuchFileException; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.nio.file.SimpleFileVisitor; | |
| import java.nio.file.StandardCopyOption; | |
| import java.nio.file.StandardOpenOption; | |
| import java.nio.file.attribute.BasicFileAttributes; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collection; | |
| import java.util.Collections; | |
| import java.util.Comparator; | |
| import java.util.HashSet; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.ListIterator; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.UUID; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.function.Consumer; | |
| import java.util.function.Function; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.Stream; | |
| import java.util.zip.CRC32; | |
| import org.junit.Assume; | |
| import org.junit.rules.TemporaryFolder; | |
| public class ToolHelper { | |
| public static String getProjectRoot() { | |
| String oracle = System.getProperty("REPO_ROOT"); | |
| if (oracle != null) { | |
| if (!oracle.endsWith("/")) { | |
| oracle = oracle + "/"; | |
| } | |
| return oracle; | |
| } | |
| String current = System.getProperty("user.dir"); | |
| if (!current.contains("d8_r8")) { | |
| return ""; | |
| } | |
| while (current.contains("d8_r8")) { | |
| current = Paths.get(current).getParent().toString(); | |
| } | |
| return current + "/"; | |
| } | |
| private static String getBuildProp(String prop) { | |
| assert prop.startsWith("BUILD_PROP_"); | |
| String value = System.getProperty(prop); | |
| assert value != null; | |
| return value; | |
| } | |
| private static List<Path> getBuildPropPaths(String prop) { | |
| return ListUtils.map(StringUtils.split(getBuildProp(prop), File.pathSeparatorChar), Paths::get); | |
| } | |
| public static List<Path> getBuildPropR8RuntimePath() { | |
| return getBuildPropPaths("BUILD_PROP_R8_RUNTIME_PATH"); | |
| } | |
| public static List<Path> getBuildPropProcessKeepRulesRuntimePath() { | |
| return getBuildPropPaths("BUILD_PROP_PROCESS_KEEP_RULES_RUNTIME_PATH"); | |
| } | |
| public static List<Path> getBuildPropKeepAnnoRuntimePath() { | |
| return getBuildPropPaths("BUILD_PROP_KEEPANNO_RUNTIME_PATH"); | |
| } | |
| public enum TestDataSourceSet { | |
| LEGACY(null), | |
| TESTS_JAVA_8("tests_java_8/build/classes/java/test"), | |
| TESTBASE_DATA_LOCATION("testbase/build/classes/java/main"), | |
| TESTS_BOOTSTRAP("tests_bootstrap/build/classes/java/test"), | |
| SPECIFIED_BY_GRADLE_PROPERTY(null); | |
| private final String destination; | |
| TestDataSourceSet(String destination) { | |
| this.destination = destination; | |
| } | |
| public boolean isLegacy() { | |
| return this == LEGACY; | |
| } | |
| public boolean isSpecifiedByGradleProperty() { | |
| return this == SPECIFIED_BY_GRADLE_PROPERTY; | |
| } | |
| public Path getBuildDir() { | |
| if (isLegacy()) { | |
| return Paths.get(BUILD_DIR, "classes", "java", "test"); | |
| } else if (isSpecifiedByGradleProperty()) { | |
| assert System.getProperty("TEST_DATA_LOCATION") != null; | |
| return Paths.get(System.getProperty("TEST_DATA_LOCATION")); | |
| } else { | |
| return Paths.get(getProjectRoot(), "d8_r8", "test_modules", destination); | |
| } | |
| } | |
| public Path getTestBaseClassLocation() { | |
| String testbaseDataLocation = System.getProperty("TESTBASE_DATA_LOCATION"); | |
| if (testbaseDataLocation != null) { | |
| return Paths.get(testbaseDataLocation); | |
| } | |
| throw new Unreachable("TESTBASE_DATA_LOCATION not set from gradle"); | |
| } | |
| public static TestDataSourceSet computeLegacyOrGradleSpecifiedLocation() { | |
| return TestDataSourceSet.SPECIFIED_BY_GRADLE_PROPERTY; | |
| } | |
| } | |
| public static final String SOURCE_DIR = getProjectRoot() + "src/"; | |
| public static final String MAIN_SOURCE_DIR = getProjectRoot() + "src/main/java/"; | |
| public static final String LIBRARY_DESUGAR_SOURCE_DIR = getProjectRoot() + "src/library_desugar/"; | |
| public static final String BUILD_DIR = getProjectRoot() + "build/"; | |
| public static final String LIBS_DIR = BUILD_DIR + "libs/"; | |
| public static final String THIRD_PARTY_DIR = getProjectRoot() + "third_party/"; | |
| public static final String PROTO_DIR = THIRD_PARTY_DIR + "proto/"; | |
| public static final String PROTO_RUNTIME_DIR = PROTO_DIR + "runtime/"; | |
| public static final String PROTO_TEST_DIR = PROTO_DIR + "test/"; | |
| public static final String TOOLS_DIR = getProjectRoot() + "tools/"; | |
| public static final String TESTS_DIR = getProjectRoot() + "src/test/"; | |
| public static final String TESTS_SOURCE_DIR = TESTS_DIR + "java/"; | |
| public static final String EXAMPLES_DIR = TESTS_DIR + "examples/"; | |
| public static final String EXAMPLES_ANDROID_O_DIR = TESTS_DIR + "examplesAndroidO/"; | |
| public static final String EXAMPLES_ANDROID_P_DIR = TESTS_DIR + "examplesAndroidP/"; | |
| public static final String KEEP_RADIUS_SOURCE_DIR = getProjectRoot() + "src/keepradius/java/"; | |
| public static final String KEEP_RADIUS_WEB_DIR = getProjectRoot() + "src/keepradius/web/"; | |
| public static final String R8_TEST_BUCKET = "r8-test-results"; | |
| public static final String LINE_SEPARATOR = StringUtils.LINE_SEPARATOR; | |
| public static final String CLASSPATH_SEPARATOR = File.pathSeparator; | |
| public static final String DEFAULT_DEX_FILENAME = "classes.dex"; | |
| public static final String DEFAULT_PROGUARD_MAP_FILE = "proguard.map"; | |
| public static final String K2JVMCompiler = "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler"; | |
| private static final String ANDROID_JAR_PATTERN = | |
| THIRD_PARTY_DIR + "android_jar/lib-v%s/android.jar"; | |
| private static final String ANDROID_API_VERSIONS_XML_PATTERN = | |
| THIRD_PARTY_DIR + "android_jar/lib-v%s/api-versions.xml"; | |
| private static final AndroidApiLevel DEFAULT_MIN_SDK = AndroidApiLevel.I; | |
| public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS = | |
| THIRD_PARTY_DIR + "proguardsettings/"; | |
| // TODO(b/270105162): These should be removed when finished transitioning. | |
| public static final Path R8_WITH_RELOCATED_DEPS_17_JAR = | |
| Paths.get(LIBS_DIR, "r8_with_relocated_deps_17.jar"); | |
| public static final Path ASSISTANT_JAR = Paths.get(LIBS_DIR, "assistant.jar"); | |
| public static final Path PROCESS_KEEP_RULES_JAR = Paths.get(LIBS_DIR, "processkeepruleslib.jar"); | |
| public static final Path PROCESS_KEEP_RULES_MAP = | |
| Paths.get(LIBS_DIR, "processkeepruleslib.jar.map"); | |
| public static final Path R8LIB_JAR = Paths.get(LIBS_DIR, "r8lib.jar"); | |
| public static final Path R8LIB_MAP = Paths.get(LIBS_DIR, "r8lib.jar.map"); | |
| public static final Path R8LIB_MAP_PARTITIONED = Paths.get(LIBS_DIR, "r8lib.jar_map.zip"); | |
| public static final Path R8LIB_EXCLUDE_DEPS_JAR = Paths.get(LIBS_DIR, "r8lib-exclude-deps.jar"); | |
| public static final Path R8LIB_EXCLUDE_DEPS_MAP = | |
| Paths.get(LIBS_DIR, "r8lib-exclude-deps.jar.map"); | |
| public static List<Path> getDeps() { | |
| assert System.getProperty("R8_DEPS") != null; | |
| return ListUtils.map( | |
| StringUtils.split(System.getProperty("R8_DEPS"), File.pathSeparatorChar), Paths::get); | |
| } | |
| public static Path getR8WithRelocatedDeps() { | |
| assert System.getProperty("R8_WITH_RELOCATED_DEPS") != null; | |
| return Paths.get(System.getProperty("R8_WITH_RELOCATED_DEPS")); | |
| } | |
| public static Path getR8SwissArmyKnife() { | |
| assert System.getProperty("R8_SWISS_ARMY_KNIFE") != null; | |
| return Paths.get(System.getProperty("R8_SWISS_ARMY_KNIFE")); | |
| } | |
| public static boolean isLocalDevelopment() { | |
| return System.getProperty("local_development", "0").equals("1"); | |
| } | |
| public static boolean shouldRunSlowTests() { | |
| return System.getProperty("slow_tests", "0").equals("1"); | |
| } | |
| public static boolean verifyValidOutputMode(Backend backend, OutputMode outputMode) { | |
| return (backend == Backend.CF && outputMode == OutputMode.ClassFile) || backend == Backend.DEX; | |
| } | |
| public static boolean isBot() { | |
| String swarming_bot_id = System.getenv("SWARMING_BOT_ID"); | |
| return swarming_bot_id != null && !swarming_bot_id.isEmpty(); | |
| } | |
| public static StringConsumer consumeString(Consumer<String> consumer) { | |
| return new StringConsumer() { | |
| private StringBuilder builder; | |
| @Override | |
| public void accept(String string, DiagnosticsHandler handler) { | |
| if (builder == null) { | |
| builder = new StringBuilder(); | |
| } | |
| builder.append(string); | |
| } | |
| @Override | |
| public void finished(DiagnosticsHandler handler) { | |
| if (builder != null) { | |
| consumer.accept(builder.toString()); | |
| } | |
| } | |
| }; | |
| } | |
| public enum DexVm { | |
| ART_4_0_4_TARGET(Version.V4_0_4, Kind.TARGET), | |
| ART_4_0_4_HOST(Version.V4_0_4, Kind.HOST), | |
| ART_4_4_4_TARGET(Version.V4_4_4, Kind.TARGET), | |
| ART_4_4_4_HOST(Version.V4_4_4, Kind.HOST), | |
| ART_5_1_1_TARGET(Version.V5_1_1, Kind.TARGET), | |
| ART_5_1_1_HOST(Version.V5_1_1, Kind.HOST), | |
| ART_6_0_1_TARGET(Version.V6_0_1, Kind.TARGET), | |
| ART_6_0_1_HOST(Version.V6_0_1, Kind.HOST), | |
| ART_7_0_0_TARGET(Version.V7_0_0, Kind.TARGET), | |
| ART_7_0_0_HOST(Version.V7_0_0, Kind.HOST), | |
| ART_DEFAULT(Version.DEFAULT, Kind.HOST), | |
| ART_8_1_0_TARGET(Version.V8_1_0, Kind.TARGET), | |
| ART_8_1_0_HOST(Version.V8_1_0, Kind.HOST), | |
| ART_9_0_0_TARGET(Version.V9_0_0, Kind.TARGET), | |
| ART_9_0_0_HOST(Version.V9_0_0, Kind.HOST), | |
| ART_10_0_0_TARGET(Version.V10_0_0, Kind.TARGET), | |
| ART_10_0_0_HOST(Version.V10_0_0, Kind.HOST), | |
| ART_12_0_0_TARGET(Version.V12_0_0, Kind.TARGET), | |
| ART_12_0_0_HOST(Version.V12_0_0, Kind.HOST), | |
| ART_13_0_0_TARGET(Version.V13_0_0, Kind.TARGET), | |
| ART_13_0_0_HOST(Version.V13_0_0, Kind.HOST), | |
| ART_14_0_0_TARGET(Version.V14_0_0, Kind.TARGET), | |
| ART_14_0_0_HOST(Version.V14_0_0, Kind.HOST), | |
| ART_15_0_0_TARGET(Version.V15_0_0, Kind.TARGET), | |
| ART_15_0_0_HOST(Version.V15_0_0, Kind.HOST), | |
| ART_16_0_0_TARGET(Version.V16_0_0, Kind.TARGET), | |
| ART_16_0_0_HOST(Version.V16_0_0, Kind.HOST), | |
| ART_17_0_0_TARGET(Version.V17_0_0, Kind.TARGET), | |
| ART_17_0_0_HOST(Version.V17_0_0, Kind.HOST), | |
| ART_17_1_0_TARGET(Version.V17_1_0, Kind.TARGET), | |
| ART_17_1_0_HOST(Version.V17_1_0, Kind.HOST), | |
| ART_17_2_0_TARGET(Version.V17_2_0, Kind.TARGET), | |
| ART_17_2_0_HOST(Version.V17_2_0, Kind.HOST), | |
| ART_MASTER_TARGET(Version.MASTER, Kind.TARGET), | |
| ART_MASTER_HOST(Version.MASTER, Kind.HOST); | |
| private static final ImmutableMap<String, DexVm> SHORT_NAME_MAP = | |
| Arrays.stream(DexVm.values()).collect(ImmutableMap.toImmutableMap( | |
| DexVm::toString, Function.identity())); | |
| public enum Version { | |
| V4_0_4("4.0.4"), | |
| V4_4_4("4.4.4"), | |
| V5_1_1("5.1.1"), | |
| V6_0_1("6.0.1"), | |
| V7_0_0("7.0.0"), | |
| V8_1_0("8.1.0"), | |
| // TODO(b/204855476): Remove DEFAULT. | |
| DEFAULT("default"), | |
| V9_0_0("9.0.0"), | |
| V10_0_0("10.0.0"), | |
| V12_0_0("12.0.0"), | |
| V13_0_0("13.0.0"), | |
| V14_0_0("14.0.0"), | |
| V15_0_0("15.0.0"), | |
| V16_0_0("16.0.0"), | |
| V17_0_0("17.0.0"), | |
| V17_1_0("17.1.0"), | |
| V17_2_0("17.2.0"), | |
| MASTER("master"); | |
| /** This should generally be the latest DEX VM fully supported. */ | |
| // TODO(b/204855476): Rename to DEFAULT alias once the checked in VM is removed. | |
| public static final Version NEW_DEFAULT = DEFAULT; | |
| public static final Version LATEST_DEX2OAT = V16_0_0; | |
| Version(String shortName) { | |
| this.shortName = shortName; | |
| } | |
| public boolean isDalvik() { | |
| return isOlderThanOrEqual(Version.V4_4_4); | |
| } | |
| public boolean isDefault() { | |
| return this == NEW_DEFAULT; | |
| } | |
| public boolean isLatest() { | |
| return this == last(); | |
| } | |
| public boolean isEqualTo(Version other) { | |
| return compareTo(other) == 0; | |
| } | |
| public boolean isEqualToOneOf(Version... versions) { | |
| return Arrays.stream(versions).anyMatch(this::isEqualTo); | |
| } | |
| public boolean isNewerThan(Version other) { | |
| return compareTo(other) > 0; | |
| } | |
| public boolean isNewerThanOrEqual(Version other) { | |
| return compareTo(other) >= 0; | |
| } | |
| public boolean isOlderThan(Version other) { | |
| return compareTo(other) < 0; | |
| } | |
| public boolean isOlderThanOrEqual(Version other) { | |
| return compareTo(other) <= 0; | |
| } | |
| public boolean isInRangeInclusive(Version start, Version end) { | |
| assert start.isOlderThanOrEqual(end); | |
| return isNewerThanOrEqual(start) && isOlderThanOrEqual(end); | |
| } | |
| public boolean hasRecordsSupport() { | |
| // Records support is present from Android U. | |
| return isNewerThanOrEqual(V14_0_0); | |
| } | |
| public boolean hasSealedClassesSupport() { | |
| // Records support is present from Android U. | |
| return isNewerThanOrEqual(V14_0_0); | |
| } | |
| public String toString() { | |
| return shortName; | |
| } | |
| private final String shortName; | |
| public static Version first() { | |
| return V4_0_4; | |
| } | |
| public static Version last() { | |
| return V17_2_0; | |
| } | |
| public static Version master() { | |
| return MASTER; | |
| } | |
| static { | |
| // Ensure first is always first and last is always last except for master. | |
| assert Arrays.stream(values()).allMatch(v -> v == first() || v.compareTo(first()) > 0); | |
| assert Arrays.stream(values()) | |
| .allMatch(v -> v == last() || v == master() || v.compareTo(last()) < 0); | |
| } | |
| } | |
| public enum Kind { | |
| HOST("host"), | |
| TARGET("target"); | |
| Kind(String shortName) { | |
| this.shortName = shortName; | |
| } | |
| public String toString() { | |
| return shortName; | |
| } | |
| private final String shortName; | |
| } | |
| public String toString() { | |
| return version.shortName + '_' + kind.shortName; | |
| } | |
| public static DexVm fromShortName(String shortName) { | |
| return SHORT_NAME_MAP.get(shortName); | |
| } | |
| public static DexVm fromVersion(Version version) { | |
| return SHORT_NAME_MAP.get(version.shortName + "_" + Kind.HOST); | |
| } | |
| public boolean isEqualTo(DexVm other) { | |
| return version.isEqualTo(other.version); | |
| } | |
| public boolean isNewerThan(DexVm other) { | |
| return version.isNewerThan(other.version); | |
| } | |
| public boolean isNewerThanOrEqual(DexVm other) { | |
| return version.isNewerThanOrEqual(other.version); | |
| } | |
| public boolean isOlderThan(DexVm other) { | |
| return version.isOlderThan(other.version); | |
| } | |
| public boolean isOlderThanOrEqual(DexVm other) { | |
| return version.isOlderThanOrEqual(other.version); | |
| } | |
| DexVm(Version version, Kind kind) { | |
| this.version = version; | |
| this.kind = kind; | |
| } | |
| public Version getVersion() { | |
| return version; | |
| } | |
| public Kind getKind() { | |
| return kind; | |
| } | |
| private final Version version; | |
| private final Kind kind; | |
| } | |
| public abstract static class CommandBuilder { | |
| protected List<String> options = new ArrayList<>(); | |
| protected Map<String, String> systemProperties = new LinkedHashMap<>(); | |
| protected List<String> classpaths = new ArrayList<>(); | |
| protected String mainClass; | |
| protected List<String> programArguments = new ArrayList<>(); | |
| protected List<String> bootClasspaths = new ArrayList<>(); | |
| protected String executionDirectory; | |
| public CommandBuilder appendArtOption(String option) { | |
| options.add(option); | |
| return this; | |
| } | |
| public CommandBuilder appendArtSystemProperty(String key, String value) { | |
| systemProperties.put(key, value); | |
| return this; | |
| } | |
| public CommandBuilder appendClasspath(String classpath) { | |
| classpaths.add(classpath); | |
| return this; | |
| } | |
| public CommandBuilder setMainClass(String className) { | |
| this.mainClass = className; | |
| return this; | |
| } | |
| public CommandBuilder appendProgramArgument(String option) { | |
| programArguments.add(option); | |
| return this; | |
| } | |
| public CommandBuilder appendBootClasspath(String lib) { | |
| bootClasspaths.add(lib); | |
| return this; | |
| } | |
| private List<String> command() { | |
| List<String> result = new ArrayList<>(); | |
| // The art script _must_ be run with bash, bots default to /bin/dash for /bin/sh, so | |
| // explicitly set it; | |
| if (shouldUseDocker()) { | |
| result.add("tools/docker/run.sh"); | |
| } else if (isLinux()) { | |
| result.add("/bin/bash"); | |
| } | |
| result.add(getExecutable()); | |
| result.addAll(options); | |
| systemProperties.putIfAbsent( | |
| "java.util.concurrent.ForkJoinPool.common.parallelism", | |
| Integer.toString(Math.max(1, Runtime.getRuntime().availableProcessors() - 1))); | |
| for (Map.Entry<String, String> entry : systemProperties.entrySet()) { | |
| StringBuilder builder = new StringBuilder("-D"); | |
| builder.append(entry.getKey()); | |
| builder.append("="); | |
| builder.append(entry.getValue()); | |
| result.add(builder.toString()); | |
| } | |
| if (!bootClasspaths.isEmpty()) { | |
| result.add("-Xbootclasspath:" + String.join(":", bootClasspaths)); | |
| } | |
| if (!classpaths.isEmpty()) { | |
| result.add("-cp"); | |
| result.add(String.join(":", classpaths)); | |
| } | |
| if (mainClass != null) { | |
| result.add(mainClass); | |
| } | |
| for (String argument : programArguments) { | |
| result.add(argument); | |
| } | |
| return result; | |
| } | |
| public ProcessBuilder asProcessBuilder() { | |
| ProcessBuilder processBuilder = new ProcessBuilder(command()); | |
| if (executionDirectory != null) { | |
| processBuilder.directory(new File(executionDirectory)); | |
| } | |
| return processBuilder; | |
| } | |
| public String build() { | |
| return String.join(" ", command()); | |
| } | |
| protected abstract boolean shouldUseDocker(); | |
| protected abstract String getExecutable(); | |
| } | |
| public static class ArtCommandBuilder extends CommandBuilder { | |
| private DexVm version; | |
| private boolean withArtFrameworks; | |
| private CacheLookupKey artResultCacheLookupKey; | |
| private boolean noCaching = false; | |
| public ArtCommandBuilder(DexVm version) { | |
| if (version.getKind() == Kind.HOST) { | |
| assert ART_BINARY_VERSIONS.containsKey(version); | |
| } | |
| this.version = version; | |
| } | |
| @Override | |
| protected boolean shouldUseDocker() { | |
| return isMac(); | |
| } | |
| public void setNoCaching(boolean noCaching) { | |
| this.noCaching = noCaching; | |
| } | |
| @Override | |
| protected String getExecutable() { | |
| if (withArtFrameworks && version.isNewerThan(DexVm.ART_4_4_4_HOST)) { | |
| // Run directly Art in its repository, which has been patched by gradle to match expected | |
| // path for the frameworks. | |
| executionDirectory = getArtDir(version); | |
| return getRawArtBinary(version); | |
| } | |
| return getArtBinary(version); | |
| } | |
| public boolean isForDevice() { | |
| return version.getKind() == Kind.TARGET; | |
| } | |
| public ArtCommandBuilder addToJavaLibraryPath(File file) { | |
| Assume.assumeTrue("JNI tests are not yet supported on devices", !isForDevice()); | |
| appendArtSystemProperty("java.library.path", file.getAbsolutePath()); | |
| return this; | |
| } | |
| public DeviceRunner asDeviceRunner() { | |
| return new DeviceRunner() | |
| .setVmOptions(options) | |
| .setSystemProperties(systemProperties) | |
| .setClasspath(toFileList(classpaths)) | |
| .setBootClasspath(toFileList(bootClasspaths)) | |
| .setMainClass(mainClass) | |
| .setProgramArguments(programArguments); | |
| } | |
| private boolean useCache() { | |
| return !noCaching && CommandResultCache.isEnabled(); | |
| } | |
| public void cacheResult(ProcessResult result) { | |
| // Only cache succeding runs, otherwise a flaky or killed art run can | |
| // put invalid entries into the cache. | |
| if (useCache() && result.exitCode == 0) { | |
| assert artResultCacheLookupKey != null; | |
| CommandResultCache.getInstance().putResult(result, artResultCacheLookupKey, null); | |
| } | |
| } | |
| public ProcessResult getCachedResults() { | |
| if (!useCache()) { | |
| return null; | |
| } | |
| assert artResultCacheLookupKey == null; | |
| // Reuse the key when storing results if this is not already cached. | |
| artResultCacheLookupKey = new CacheLookupKey(this::hashParts); | |
| Pair<ProcessResult, Path> lookup = | |
| CommandResultCache.getInstance().lookup(artResultCacheLookupKey); | |
| return lookup == null ? null : lookup.getFirst(); | |
| } | |
| private void hashParts(Hasher hasher) { | |
| // Call getExecutable first, this will set executionDirectory if needed. | |
| hasher.putString(this.getExecutable(), StandardCharsets.UTF_8); | |
| if (this.executionDirectory != null) { | |
| hasher.putString(this.executionDirectory, StandardCharsets.UTF_8); | |
| } | |
| hasher.putString(this.mainClass, StandardCharsets.UTF_8); | |
| hasher.putBoolean(this.withArtFrameworks); | |
| hashFilesFromList(hasher, classpaths); | |
| hashFilesFromList(hasher, bootClasspaths); | |
| systemProperties.forEach( | |
| (s, t) -> | |
| hasher.putString(s, StandardCharsets.UTF_8).putString(t, StandardCharsets.UTF_8)); | |
| programArguments.forEach(s -> hasher.putString(s, StandardCharsets.UTF_8)); | |
| options.forEach(s -> hasher.putString(s, StandardCharsets.UTF_8)); | |
| } | |
| private static void hashFilesFromList(Hasher hasher, List<String> files) { | |
| for (String file : files) { | |
| try { | |
| hasher.putBytes(Files.readAllBytes(Paths.get(file))); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| } | |
| public static class CacheLookupKey { | |
| private final Consumer<Hasher> hasherConsumer; | |
| private String hash; | |
| public CacheLookupKey(Consumer<Hasher> hasherConsumer) { | |
| this.hasherConsumer = hasherConsumer; | |
| } | |
| public String getHash() { | |
| if (hash == null) { | |
| Hasher hasher = Hashing.sha256().newHasher(); | |
| hasherConsumer.accept(hasher); | |
| hash = hasher.hash().toString(); | |
| } | |
| return hash; | |
| } | |
| } | |
| public static class CommandCacheStatistics { | |
| public static CommandCacheStatistics INSTANCE = new CommandCacheStatistics(); | |
| private final Path cachePutCounter; | |
| private final Path cacheMissCounter; | |
| private final Path cacheHitCounter; | |
| private CommandCacheStatistics() { | |
| String commandCacheStatsDir = System.getProperty("command_cache_stats_dir"); | |
| if (commandCacheStatsDir != null) { | |
| String processSpecificUUID = UUID.randomUUID().toString(); | |
| cachePutCounter = Paths.get(commandCacheStatsDir, processSpecificUUID + "CACHEPUT"); | |
| cacheMissCounter = Paths.get(commandCacheStatsDir, processSpecificUUID + "CACHEMISS"); | |
| cacheHitCounter = Paths.get(commandCacheStatsDir, processSpecificUUID + "CACHEHIT"); | |
| try { | |
| Files.createFile(cachePutCounter); | |
| Files.createFile(cacheMissCounter); | |
| Files.createFile(cacheHitCounter); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } else { | |
| cachePutCounter = null; | |
| cacheMissCounter = null; | |
| cacheHitCounter = null; | |
| } | |
| } | |
| private static void increaseCount(Path path) { | |
| // Not enabled | |
| if (path == null) { | |
| return; | |
| } | |
| synchronized (path) { | |
| try { | |
| Files.write(path, "X".getBytes(StandardCharsets.UTF_8), StandardOpenOption.APPEND); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| public void addCachePut() { | |
| increaseCount(cachePutCounter); | |
| } | |
| public void addCacheHit() { | |
| increaseCount(cacheHitCounter); | |
| } | |
| public void addCacheMiss() { | |
| increaseCount(cacheMissCounter); | |
| } | |
| } | |
| public static class CommandResultCache { | |
| private static CommandResultCache INSTANCE = | |
| System.getProperty("command_cache_dir") != null | |
| ? new CommandResultCache(Paths.get(System.getProperty("command_cache_dir"))) | |
| : null; | |
| private final Path path; | |
| public CommandResultCache(Path path) { | |
| this.path = path; | |
| } | |
| public static CommandResultCache getInstance() { | |
| return INSTANCE; | |
| } | |
| public static boolean isEnabled() { | |
| return getInstance() != null; | |
| } | |
| private Path getStdoutFile(CacheLookupKey cacheLookupKey) { | |
| return path.resolve(cacheLookupKey.getHash() + ".stdout"); | |
| } | |
| private Path getStderrFile(CacheLookupKey cacheLookupKey) { | |
| return path.resolve(cacheLookupKey.getHash() + ".stderr"); | |
| } | |
| private Path getOutputFile(CacheLookupKey cacheLookupKey) { | |
| return path.resolve(cacheLookupKey.getHash() + ".output"); | |
| } | |
| private Path getExitCodeFile(CacheLookupKey cacheLookupKey) { | |
| return path.resolve(cacheLookupKey.getHash()); | |
| } | |
| private Path getTempFile(Path path) { | |
| return Paths.get(path.toString() + ".temp" + UUID.randomUUID()); | |
| } | |
| private String getStringContent(Path path) throws IOException { | |
| return FileUtils.readTextFile(path, Charsets.UTF_8); | |
| } | |
| // Hashes of entries read or written by the currently running test. Tests run sequentially | |
| // within a test worker JVM, so a single set (rather than a thread local) also covers commands | |
| // started on helper threads. | |
| private final Set<String> touchedHashes = new HashSet<>(); | |
| private void recordTouched(CacheLookupKey cacheLookupKey) { | |
| synchronized (touchedHashes) { | |
| touchedHashes.add(cacheLookupKey.getHash()); | |
| } | |
| } | |
| public static void onTestStarted() { | |
| if (isEnabled()) { | |
| synchronized (INSTANCE.touchedHashes) { | |
| INSTANCE.touchedHashes.clear(); | |
| } | |
| } | |
| } | |
| // A command can exit successfully with incorrect output (e.g., flaky stdout from art). Remove | |
| // all entries touched by a failing test so that retries rerun the commands instead of | |
| // replaying the cached result. | |
| public static void onTestFailed() { | |
| if (!isEnabled()) { | |
| return; | |
| } | |
| List<String> hashes; | |
| synchronized (INSTANCE.touchedHashes) { | |
| hashes = new ArrayList<>(INSTANCE.touchedHashes); | |
| INSTANCE.touchedHashes.clear(); | |
| } | |
| for (String hash : hashes) { | |
| // Delete the exit code file first, as its presence marks the entry as valid. | |
| for (String suffix : new String[] {"", ".stdout", ".stderr", ".output"}) { | |
| try { | |
| Files.deleteIfExists(INSTANCE.path.resolve(hash + suffix)); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| } | |
| public Pair<ProcessResult, Path> lookup(CacheLookupKey cacheLookupKey) { | |
| Path exitCodeFile = getExitCodeFile(cacheLookupKey); | |
| if (exitCodeFile.toFile().exists()) { | |
| // Because of the temp files and order of writing we should never get here with an | |
| // inconsistent state. It is possible, although unlikely, that the stdout/stderr | |
| // (and even exitcode if art is non deterministic) are from different, process ids etc, | |
| // but this should have no impact. | |
| try { | |
| int exitCode = Integer.parseInt(getStringContent(exitCodeFile)); | |
| String stdout = getStringContent(getStdoutFile(cacheLookupKey)); | |
| String stderr = getStringContent(getStderrFile(cacheLookupKey)); | |
| Path outputFile = getOutputFile(cacheLookupKey); | |
| recordTouched(cacheLookupKey); | |
| CommandCacheStatistics.INSTANCE.addCacheHit(); | |
| return new Pair<>( | |
| new ProcessResult(exitCode, stdout, stderr), | |
| outputFile.toFile().exists() ? outputFile : null); | |
| } catch (NoSuchFileException e) { | |
| // The entry was invalidated concurrently, treat it as a miss. | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| CommandCacheStatistics.INSTANCE.addCacheMiss(); | |
| return null; | |
| } | |
| public void putResult(ProcessResult result, CacheLookupKey cacheLookupKey, Path output) { | |
| Path exitCodeFile = getExitCodeFile(cacheLookupKey); | |
| Path exitCodeTempFile = getTempFile(exitCodeFile); | |
| Path stdoutFile = getStdoutFile(cacheLookupKey); | |
| Path stdoutTempFile = getTempFile(stdoutFile); | |
| Path stderrFile = getStderrFile(cacheLookupKey); | |
| Path stderrTempFile = getTempFile(stderrFile); | |
| Path outputFile = getOutputFile(cacheLookupKey); | |
| Path outputTempFile = getTempFile(outputFile); | |
| try { | |
| String exitCode = "" + result.exitCode; | |
| // We avoid race conditions of writing vs reading by first writing all 3 files to temp | |
| // files, then moving these to the result files, moving last the exitcode file (which is | |
| // what we use as cache present check) | |
| Files.write(exitCodeTempFile, exitCode.getBytes(StandardCharsets.UTF_8)); | |
| Files.write(stdoutTempFile, result.stdout.getBytes(StandardCharsets.UTF_8)); | |
| Files.write(stderrTempFile, result.stderr.getBytes(StandardCharsets.UTF_8)); | |
| if (output != null) { | |
| Files.copy(output, outputTempFile); | |
| } | |
| // Order is important, move exitcode file last! | |
| Files.move( | |
| stdoutTempFile, | |
| stdoutFile, | |
| StandardCopyOption.ATOMIC_MOVE, | |
| StandardCopyOption.REPLACE_EXISTING); | |
| Files.move( | |
| stderrTempFile, | |
| stderrFile, | |
| StandardCopyOption.ATOMIC_MOVE, | |
| StandardCopyOption.REPLACE_EXISTING); | |
| if (output != null) { | |
| Files.move( | |
| outputTempFile, | |
| outputFile, | |
| StandardCopyOption.ATOMIC_MOVE, | |
| StandardCopyOption.REPLACE_EXISTING); | |
| } | |
| Files.move( | |
| exitCodeTempFile, | |
| exitCodeFile, | |
| StandardCopyOption.ATOMIC_MOVE, | |
| StandardCopyOption.REPLACE_EXISTING); | |
| recordTouched(cacheLookupKey); | |
| CommandCacheStatistics.INSTANCE.addCachePut(); | |
| } catch (IOException e) { | |
| StringBuilder exceptionMessage = new StringBuilder(); | |
| exceptionMessage.append( | |
| "Files.exists(exitCodeTempFile) = " + Files.exists(exitCodeTempFile)); | |
| exceptionMessage.append("Files.exists(stdoutTempFile) = " + Files.exists(stdoutTempFile)); | |
| exceptionMessage.append("Files.exists(stderrTempFile) = " + Files.exists(stderrTempFile)); | |
| exceptionMessage.append("Files.exists(outputTempFile) = " + Files.exists(outputTempFile)); | |
| throw new RuntimeException(exceptionMessage.toString(), e); | |
| } | |
| } | |
| } | |
| private static List<File> toFileList(List<String> filePathList) { | |
| return filePathList.stream().map(File::new).collect(Collectors.toList()); | |
| } | |
| private static class StreamReader implements Runnable { | |
| private final InputStream stream; | |
| private final StringBuilder result = new StringBuilder(); | |
| public StreamReader(InputStream stream) { | |
| this.stream = stream; | |
| } | |
| public synchronized String getResult() { | |
| return result.toString(); | |
| } | |
| @Override | |
| public void run() { | |
| try (Reader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { | |
| char[] buffer = new char[8192]; | |
| int read; | |
| while ((read = reader.read(buffer)) != -1) { | |
| synchronized (this) { | |
| result.append(buffer, 0, read); | |
| } | |
| } | |
| } catch (IOException e) { | |
| synchronized (this) { | |
| if (result.length() > 0) { | |
| result.append('\n'); | |
| } | |
| result.append("Failed reading result for stream ").append(stream); | |
| } | |
| } | |
| } | |
| } | |
| private static final Map<DexVm, String> ART_DIRS = | |
| ImmutableMap.<DexVm, String>builder() | |
| .put(DexVm.ART_DEFAULT, "art") | |
| .put(DexVm.ART_MASTER_HOST, "host/art-master") | |
| .put(DexVm.ART_17_2_0_HOST, "host/art-17.2.0") | |
| .put(DexVm.ART_17_1_0_HOST, "host/art-17.1.0") | |
| .put(DexVm.ART_17_0_0_HOST, "host/art-17.0.0-beta4") | |
| .put(DexVm.ART_16_0_0_HOST, "host/art-16.0.0") | |
| .put(DexVm.ART_15_0_0_HOST, "host/art-15.0.0-beta2") | |
| .put(DexVm.ART_14_0_0_HOST, "host/art-14.0.0-beta3") | |
| .put(DexVm.ART_13_0_0_HOST, "host/art-13.0.0") | |
| .put(DexVm.ART_12_0_0_HOST, "host/art-12.0.0-beta4") | |
| .put(DexVm.ART_10_0_0_HOST, "art-10.0.0") | |
| .put(DexVm.ART_9_0_0_HOST, "art-9.0.0") | |
| .put(DexVm.ART_8_1_0_HOST, "art-8.1.0") | |
| .put(DexVm.ART_7_0_0_HOST, "art-7.0.0") | |
| .put(DexVm.ART_6_0_1_HOST, "art-6.0.1") | |
| .put(DexVm.ART_5_1_1_HOST, "art-5.1.1") | |
| .put(DexVm.ART_4_4_4_HOST, "dalvik") | |
| .put(DexVm.ART_4_0_4_HOST, "dalvik-4.0.4") | |
| .build(); | |
| private static final Map<DexVm, String> ART_BINARY_VERSIONS = | |
| ImmutableMap.<DexVm, String>builder() | |
| .put(DexVm.ART_DEFAULT, "bin/art") | |
| .put(DexVm.ART_MASTER_HOST, "bin/art") | |
| .put(DexVm.ART_17_2_0_HOST, "bin/art") | |
| .put(DexVm.ART_17_1_0_HOST, "bin/art") | |
| .put(DexVm.ART_17_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_16_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_15_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_14_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_13_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_12_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_10_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_9_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_8_1_0_HOST, "bin/art") | |
| .put(DexVm.ART_7_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_6_0_1_HOST, "bin/art") | |
| .put(DexVm.ART_5_1_1_HOST, "bin/art") | |
| .put(DexVm.ART_4_4_4_HOST, "bin/dalvik") | |
| .put(DexVm.ART_4_0_4_HOST, "bin/dalvik") | |
| .build(); | |
| private static final Map<DexVm, String> ART_BINARY_VERSIONS_X64 = | |
| ImmutableMap.<DexVm, String>builder() | |
| .put(DexVm.ART_DEFAULT, "bin/art") | |
| .put(DexVm.ART_17_2_0_HOST, "bin/art") | |
| .put(DexVm.ART_17_1_0_HOST, "bin/art") | |
| .put(DexVm.ART_17_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_16_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_15_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_14_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_13_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_12_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_10_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_9_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_8_1_0_HOST, "bin/art") | |
| .put(DexVm.ART_7_0_0_HOST, "bin/art") | |
| .put(DexVm.ART_6_0_1_HOST, "bin/art") | |
| .build(); | |
| private static final List<String> DALVIK_4_0_BOOT_LIBS = | |
| ImmutableList.of("core-hostdex.jar", "apache-xml-hostdex.jar"); | |
| private static final List<String> DALVIK_4_4_BOOT_LIBS = | |
| ImmutableList.of("core-libart-hostdex.jar", "core-hostdex.jar", "apache-xml-hostdex.jar"); | |
| private static final List<String> ART_5_TO_6_BOOT_LIBS = | |
| ImmutableList.of("core-libart-hostdex.jar"); | |
| private static final List<String> ART_7_TO_10_BOOT_LIBS = | |
| ImmutableList.of("core-libart-hostdex.jar", "core-oj-hostdex.jar", "apache-xml-hostdex.jar"); | |
| private static final List<String> ART_12_PLUS_BOOT_LIBS = | |
| ImmutableList.of( | |
| "core-libart-hostdex.jar", | |
| "core-oj-hostdex.jar", | |
| "core-icu4j-hostdex.jar", | |
| "apache-xml-hostdex.jar"); | |
| private static final Map<DexVm, List<String>> BOOT_LIBS; | |
| static { | |
| ImmutableMap.Builder<DexVm, List<String>> builder = ImmutableMap.builder(); | |
| builder | |
| .put(DexVm.ART_DEFAULT, ART_7_TO_10_BOOT_LIBS) | |
| .put(DexVm.ART_17_2_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_17_1_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_17_0_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_16_0_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_15_0_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_14_0_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_13_0_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_12_0_0_HOST, ART_12_PLUS_BOOT_LIBS) | |
| .put(DexVm.ART_10_0_0_HOST, ART_7_TO_10_BOOT_LIBS) | |
| .put(DexVm.ART_9_0_0_HOST, ART_7_TO_10_BOOT_LIBS) | |
| .put(DexVm.ART_8_1_0_HOST, ART_7_TO_10_BOOT_LIBS) | |
| .put(DexVm.ART_7_0_0_HOST, ART_7_TO_10_BOOT_LIBS) | |
| .put(DexVm.ART_6_0_1_HOST, ART_5_TO_6_BOOT_LIBS) | |
| .put(DexVm.ART_5_1_1_HOST, ART_5_TO_6_BOOT_LIBS) | |
| .put(DexVm.ART_4_4_4_HOST, DALVIK_4_4_BOOT_LIBS) | |
| .put(DexVm.ART_4_0_4_HOST, DALVIK_4_0_BOOT_LIBS); | |
| BOOT_LIBS = builder.build(); | |
| } | |
| private static final Map<DexVm, String> PRODUCT; | |
| static { | |
| ImmutableMap.Builder<DexVm, String> builder = ImmutableMap.builder(); | |
| builder | |
| .put(DexVm.ART_DEFAULT, "angler") | |
| .put(DexVm.ART_17_2_0_HOST, "akita") | |
| .put(DexVm.ART_17_1_0_HOST, "akita") | |
| .put(DexVm.ART_17_0_0_HOST, "akita") | |
| .put(DexVm.ART_16_0_0_HOST, "akita") | |
| .put(DexVm.ART_15_0_0_HOST, "akita") | |
| .put(DexVm.ART_14_0_0_HOST, "redfin") | |
| .put(DexVm.ART_13_0_0_HOST, "redfin") | |
| .put(DexVm.ART_12_0_0_HOST, "redfin") | |
| .put(DexVm.ART_10_0_0_HOST, "coral") | |
| .put(DexVm.ART_9_0_0_HOST, "marlin") | |
| .put(DexVm.ART_8_1_0_HOST, "marlin") | |
| .put(DexVm.ART_7_0_0_HOST, "angler") | |
| .put(DexVm.ART_6_0_1_HOST, "angler") | |
| .put(DexVm.ART_5_1_1_HOST, "mako") | |
| .put(DexVm.ART_4_4_4_HOST, "<missing>") | |
| .put(DexVm.ART_4_0_4_HOST, "<missing>"); | |
| PRODUCT = builder.build(); | |
| } | |
| private static Path getDexVmPath(DexVm vm) { | |
| DexVm.Version version = vm.getVersion(); | |
| Path base = Paths.get(TOOLS_DIR, "linux"); | |
| switch (version) { | |
| case DEFAULT: | |
| return base.resolve("art"); | |
| case V4_0_4: | |
| case V4_4_4: | |
| case V5_1_1: | |
| case V6_0_1: | |
| case V7_0_0: | |
| case V8_1_0: | |
| case V9_0_0: | |
| case V10_0_0: | |
| return base.resolve("art-" + version); | |
| case V12_0_0: | |
| return base.resolve("host").resolve("art-12.0.0-beta4"); | |
| case V13_0_0: | |
| case V14_0_0: | |
| case V15_0_0: | |
| case V16_0_0: | |
| case V17_0_0: | |
| case V17_1_0: | |
| case V17_2_0: | |
| case MASTER: | |
| return base.resolve("host").resolve("art-" + version); | |
| default: | |
| throw new Unreachable(); | |
| } | |
| } | |
| private static Path getDexVmLibPath(DexVm vm) { | |
| return getDexVmPath(vm).resolve("lib"); | |
| } | |
| private static Path getDex2OatPath(DexVm vm) { | |
| return getDexVmPath(vm).resolve("bin").resolve("dex2oat"); | |
| } | |
| private static Path getProductPath(DexVm vm) { | |
| return getDexVmPath(vm).resolve("product").resolve(PRODUCT.get(vm)); | |
| } | |
| private static String getArchString(DexVm vm) { | |
| switch (vm.getVersion()) { | |
| case V4_0_4: | |
| case V4_4_4: | |
| case V5_1_1: | |
| return "arm"; | |
| case V6_0_1: | |
| case V7_0_0: | |
| case V8_1_0: | |
| case DEFAULT: | |
| case V9_0_0: | |
| case V10_0_0: | |
| case V13_0_0: | |
| case V14_0_0: | |
| case V15_0_0: | |
| case V16_0_0: | |
| case V17_0_0: | |
| case V17_1_0: | |
| case V17_2_0: | |
| case MASTER: | |
| return "arm64"; | |
| case V12_0_0: | |
| return "x86_64"; | |
| default: | |
| throw new Unimplemented(); | |
| } | |
| } | |
| public static byte[] getClassAsBytes(Class<?> clazz) { | |
| try { | |
| return Files.readAllBytes(getClassFileForTestClass(clazz)); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } | |
| } | |
| public static byte[] getClassAsBytes(Class<?> clazz, TestDataSourceSet dataSourceSet) | |
| throws IOException { | |
| return Files.readAllBytes(getClassFileForTestClass(clazz, dataSourceSet)); | |
| } | |
| public static long getClassByteCrc(Class<?> clazz) { | |
| byte[] bytes = getClassAsBytes(clazz); | |
| CRC32 crc = new CRC32(); | |
| crc.update(bytes, 0, bytes.length); | |
| return crc.getValue(); | |
| } | |
| public static String getArtDir(DexVm version) { | |
| String dir = ART_DIRS.get(version); | |
| if (dir == null) { | |
| throw new IllegalStateException("Does not support dex vm: " + version); | |
| } | |
| if (isLinux() || isMac()) { | |
| // The Linux version is used on Mac, where it is run in a Docker container. | |
| return TOOLS_DIR + "linux/" + dir; | |
| } | |
| fail("Unsupported platform, we currently only support mac and linux: " + getPlatform()); | |
| return ""; //never here | |
| } | |
| public static String getArtBinary(DexVm version) { | |
| return getArtDir(version) + "/" + getRawArtBinary(version); | |
| } | |
| public static String getRawArtBinary(DexVm version) { | |
| String binary = ART_BINARY_VERSIONS.get(version); | |
| if (binary == null) { | |
| throw new IllegalStateException("Does not support running with dex vm: " + version); | |
| } | |
| return binary; | |
| } | |
| @Deprecated | |
| // Use getFirstSupportedAndroidJar(AndroidApiLevel) to specify a specific Android jar. | |
| public static Path getDefaultAndroidJar() { | |
| return getAndroidJar(AndroidApiLevel.getDefault()); | |
| } | |
| public static Path getFirstSupportedAndroidJar(AndroidApiLevel apiLevel) { | |
| // Fast path. | |
| if (hasAndroidJar(apiLevel)) { | |
| return getAndroidJar(apiLevel); | |
| } | |
| // Search for an android jar. | |
| for (AndroidApiLevel level : AndroidApiLevel.getAndroidApiLevelsSorted()) { | |
| if (level.isGreaterThanOrEqualTo(apiLevel) && hasAndroidJar(level)) { | |
| return getAndroidJar(level); | |
| } | |
| } | |
| return getAndroidJar(AndroidApiLevel.LATEST); | |
| } | |
| public static Path getAndroidJar(int apiLevelMajor, int apiLevelMinor) { | |
| return getAndroidJar(new UncheckedApiLevel(apiLevelMajor, apiLevelMinor)); | |
| } | |
| public static Path getAndroidJar(UncheckedApiLevel apiLevel) { | |
| return getAndroidJar(AndroidApiLevel.getAndroidApiLevel(apiLevel)); | |
| } | |
| private static String formatApiLevel(AndroidApiLevel apiLevel) { | |
| return apiLevel.getNumericString(); | |
| } | |
| public static Path getApiVersionsXmlFile(AndroidApiLevel apiLevel) { | |
| // We only store api-versions.xml from S and up. | |
| assert apiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.S); | |
| return Paths.get(String.format(ANDROID_API_VERSIONS_XML_PATTERN, formatApiLevel(apiLevel))); | |
| } | |
| private static Path getAndroidJarPath(AndroidApiLevel apiLevel) { | |
| if (apiLevel == AndroidApiLevel.MAIN) { | |
| return Paths.get(THIRD_PARTY_DIR + "android_jar/lib-main/android.jar"); | |
| } | |
| String jar = | |
| String.format( | |
| ANDROID_JAR_PATTERN, | |
| formatApiLevel(apiLevel == AndroidApiLevel.getDefault() ? DEFAULT_MIN_SDK : apiLevel)); | |
| return Paths.get(jar); | |
| } | |
| public static boolean hasAndroidJar(AndroidApiLevel apiLevel) { | |
| Path path = getAndroidJarPath(apiLevel); | |
| return Files.exists(path); | |
| } | |
| static final Set<AndroidApiLevel> apiLevelWithoutAndroidJar = | |
| ImmutableSet.<AndroidApiLevel>builder() | |
| .add(AndroidApiLevel.B_1_1) | |
| .add(AndroidApiLevel.C) | |
| .add(AndroidApiLevel.D) | |
| .add(AndroidApiLevel.E) | |
| .add(AndroidApiLevel.E_0_1) | |
| .add(AndroidApiLevel.E_MR1) | |
| .add(AndroidApiLevel.F) | |
| .add(AndroidApiLevel.G) | |
| .add(AndroidApiLevel.G_MR1) | |
| .add(AndroidApiLevel.H) | |
| .add(AndroidApiLevel.H_MR1) | |
| .add(AndroidApiLevel.H_MR2) | |
| .add(AndroidApiLevel.J) | |
| .add(AndroidApiLevel.J_MR1) | |
| .add(AndroidApiLevel.J_MR2) | |
| .add(AndroidApiLevel.K_WATCH) | |
| .add(AndroidApiLevel.EXTENSION) | |
| .build(); | |
| public static boolean shouldHaveAndroidJar(AndroidApiLevel apiLevel) { | |
| return !apiLevelWithoutAndroidJar.contains(apiLevel); | |
| } | |
| public static Path getAndroidJar(AndroidApiLevel apiLevel) { | |
| Path path = getAndroidJarPath(apiLevel); | |
| assert Files.exists(path) | |
| : "Expected android jar to exist for API level " + apiLevel + " at " + path; | |
| return path; | |
| } | |
| public static Path getLatestAndroidJar() { | |
| assert hasAndroidJar(AndroidApiLevel.LATEST); | |
| return getAndroidJar(AndroidApiLevel.LATEST); | |
| } | |
| public static Path getMostRecentAndroidJar() { | |
| List<AndroidApiLevel> apiLevels = AndroidApiLevel.getAndroidApiLevelsSorted(); | |
| ListIterator<AndroidApiLevel> iterator = apiLevels.listIterator(apiLevels.size()); | |
| while (iterator.hasPrevious()) { | |
| AndroidApiLevel apiLevel = iterator.previous(); | |
| if (hasAndroidJar(apiLevel)) { | |
| return getAndroidJar(apiLevel); | |
| } | |
| } | |
| throw new Unreachable("Unable to find a most recent android.jar"); | |
| } | |
| /** | |
| * Get the junit jar bundled with the framework. | |
| */ | |
| public static Path getFrameworkJunitJarPath(DexVm version) { | |
| return Paths.get(getArtDir(version), "framework", "junit.jar"); | |
| } | |
| static class RetainedTemporaryFolder extends TemporaryFolder { | |
| RetainedTemporaryFolder(java.io.File parentFolder) { | |
| super(parentFolder); | |
| } | |
| @Override | |
| protected void after() {} // instead of remove, do nothing | |
| } | |
| public static Path getR8TempPath() { | |
| String tmpDir = System.getProperty("test_dir"); | |
| if (tmpDir != null) { | |
| return Paths.get(tmpDir); | |
| } | |
| return Paths.get(System.getProperty("java.io.tmpdir"), "r8-temp"); | |
| } | |
| // For non-Linux platforms create the temporary directory in the repository root to simplify | |
| // running Art in a docker container | |
| public static TemporaryFolder getTemporaryFolderForTest() { | |
| String tmpDir = System.getProperty("test_dir"); | |
| if (tmpDir == null) { | |
| File r8Temp = getR8TempPath().toFile(); | |
| r8Temp.mkdirs(); | |
| return new TemporaryFolder(r8Temp); | |
| } else { | |
| return new RetainedTemporaryFolder(new java.io.File(tmpDir)); | |
| } | |
| } | |
| public static Set<DexVm> getArtVersions() { | |
| String artVersion = System.getProperty("dex_vm"); | |
| if (artVersion != null) { | |
| DexVm artVersionEnum = getDexVmFromProperty(); | |
| if (artVersionEnum.getKind() == Kind.HOST | |
| && !ART_BINARY_VERSIONS.containsKey(artVersionEnum)) { | |
| throw new RuntimeException("Unsupported Art version " + artVersion); | |
| } | |
| return ImmutableSet.of(artVersionEnum); | |
| } else { | |
| if (isWindows()) { | |
| return Collections.emptySet(); | |
| } else if (isLinux()) { | |
| return ART_BINARY_VERSIONS.keySet(); | |
| } else { | |
| return ART_BINARY_VERSIONS_X64.keySet(); | |
| } | |
| } | |
| } | |
| public static List<String> getBootLibs(DexVm dexVm) { | |
| String prefix = getArtDir(dexVm) + "/"; | |
| List<String> result = new ArrayList<>(); | |
| BOOT_LIBS.get(dexVm).stream().forEach(x -> result.add(prefix + "framework/" + x)); | |
| return result; | |
| } | |
| // Returns if the passed in vm to use is the default. | |
| public static boolean isDefaultDexVm(DexVm dexVm) { | |
| return dexVm == DexVm.ART_DEFAULT; | |
| } | |
| private static DexVm getDexVmFromProperty() { | |
| String artVersion = System.getProperty("dex_vm"); | |
| if (artVersion == null) { | |
| return DexVm.ART_DEFAULT; | |
| } else { | |
| DexVm artVersionEnum = DexVm.fromShortName(artVersion); | |
| if (artVersionEnum == null | |
| && !artVersion.endsWith(Kind.HOST.toString()) | |
| && !artVersion.endsWith(Kind.TARGET.toString())) { | |
| // Default to host Art/Dalvik when not specified. | |
| artVersionEnum = DexVm.fromShortName(artVersion + '_' + Kind.HOST.toString()); | |
| } | |
| if (artVersionEnum == null) { | |
| throw new RuntimeException("Unsupported Art version " + artVersion); | |
| } else { | |
| return artVersionEnum; | |
| } | |
| } | |
| } | |
| public static AndroidApiLevel getMinApiLevelForDexVm(DexVm dexVm) { | |
| switch (dexVm.version) { | |
| case MASTER: | |
| return AndroidApiLevel.MAIN; | |
| case V17_2_0: | |
| return AndroidApiLevel.CINNAMON_BUN_2; | |
| case V17_1_0: | |
| return AndroidApiLevel.CINNAMON_BUN_1; | |
| case V17_0_0: | |
| return AndroidApiLevel.CINNAMON_BUN; | |
| case V16_0_0: | |
| return AndroidApiLevel.BAKLAVA; | |
| case V15_0_0: | |
| return AndroidApiLevel.V; | |
| case V14_0_0: | |
| return AndroidApiLevel.U; | |
| case V13_0_0: | |
| return AndroidApiLevel.T; | |
| case V12_0_0: | |
| return AndroidApiLevel.S; | |
| case V10_0_0: | |
| return AndroidApiLevel.Q; | |
| case V9_0_0: | |
| return AndroidApiLevel.P; | |
| case V8_1_0: | |
| return AndroidApiLevel.O_MR1; | |
| case DEFAULT: | |
| return AndroidApiLevel.O; | |
| case V7_0_0: | |
| return AndroidApiLevel.N; | |
| case V6_0_1: | |
| return AndroidApiLevel.M; | |
| case V5_1_1: | |
| return AndroidApiLevel.L_MR1; | |
| case V4_4_4: | |
| return AndroidApiLevel.K; | |
| case V4_0_4: | |
| return AndroidApiLevel.I_MR1; | |
| default: | |
| throw new Unreachable("Missing min api level for dex vm " + dexVm); | |
| } | |
| } | |
| static final Map<AndroidApiLevel, DexVm.Version> apiLevelToDexVmVersion = | |
| ImmutableMap.<AndroidApiLevel, DexVm.Version>builder() | |
| .put(AndroidApiLevel.MAIN, DexVm.Version.MASTER) | |
| .put(AndroidApiLevel.CINNAMON_BUN_2, DexVm.Version.V17_2_0) | |
| .put(AndroidApiLevel.CINNAMON_BUN_1, DexVm.Version.V17_1_0) | |
| .put(AndroidApiLevel.CINNAMON_BUN, DexVm.Version.V17_0_0) | |
| .put(AndroidApiLevel.BAKLAVA_1, DexVm.Version.V16_0_0) | |
| .put(AndroidApiLevel.BAKLAVA, DexVm.Version.V16_0_0) | |
| .put(AndroidApiLevel.V, DexVm.Version.V15_0_0) | |
| .put(AndroidApiLevel.U, DexVm.Version.V14_0_0) | |
| .put(AndroidApiLevel.T, DexVm.Version.V13_0_0) | |
| .put(AndroidApiLevel.Sv2, DexVm.Version.V12_0_0) | |
| .put(AndroidApiLevel.S, DexVm.Version.V12_0_0) | |
| .put(AndroidApiLevel.Q, DexVm.Version.V10_0_0) | |
| .put(AndroidApiLevel.P, DexVm.Version.V9_0_0) | |
| .put(AndroidApiLevel.O_MR1, DexVm.Version.V8_1_0) | |
| .put(AndroidApiLevel.O, DexVm.Version.V8_1_0) | |
| .put(AndroidApiLevel.N, DexVm.Version.V7_0_0) | |
| .put(AndroidApiLevel.M, DexVm.Version.V6_0_1) | |
| .put(AndroidApiLevel.L_MR1, DexVm.Version.V5_1_1) | |
| .put(AndroidApiLevel.K, DexVm.Version.V4_4_4) | |
| .put(AndroidApiLevel.I_MR1, DexVm.Version.V4_0_4) | |
| .build(); | |
| public static DexVm.Version getDexVersionForApiLevel(AndroidApiLevel apiLevel) { | |
| DexVm.Version version = apiLevelToDexVmVersion.get(apiLevel); | |
| if (version != null) { | |
| return version; | |
| } | |
| throw new Unreachable("No Android VM for API level " + apiLevel.getNumericString()); | |
| } | |
| public static DexVersion getDexFileVersionForVm(DexVm vm) { | |
| return DexVersion.getDexVersion(getMinApiLevelForDexVm(vm)); | |
| } | |
| private static String getPlatform() { | |
| return System.getProperty("os.name"); | |
| } | |
| public static boolean isLinux() { | |
| return getPlatform().startsWith("Linux"); | |
| } | |
| public static boolean isMac() { | |
| return getPlatform().startsWith("Mac"); | |
| } | |
| public static boolean isWindows() { | |
| return getPlatform().startsWith("Windows"); | |
| } | |
| public static boolean isJava8Runtime() { | |
| return System.getProperty("java.specification.version").equals("8"); | |
| } | |
| public static boolean isJava9Runtime() { | |
| return System.getProperty("java.specification.version").equals("9"); | |
| } | |
| public static boolean isTestingR8Lib() { | |
| return System.getProperty("java.class.path").contains("r8lib.jar"); | |
| } | |
| public static boolean artSupported() { | |
| if (!isLinux() && !isMac() && !isWindows()) { | |
| System.err.println("Testing on your platform is not fully supported. " + | |
| "Art does not work on on your platform."); | |
| return false; | |
| } | |
| if (isWindows() && getDexVmFromProperty().getKind() == Kind.HOST) { | |
| System.err.println("Testing on host is not supported on Windows."); | |
| return false; | |
| } | |
| return true; | |
| } | |
| public static boolean isDex2OatSupported() { | |
| return !isWindows(); | |
| } | |
| public static Path getClassPathForTests() { | |
| return getClassPathForTestDataSourceSet(computeLegacyOrGradleSpecifiedLocation()); | |
| } | |
| public static Path getTestBaseclassPath() { | |
| return computeLegacyOrGradleSpecifiedLocation().getTestBaseClassLocation(); | |
| } | |
| public static Path getClassPathForTestDataSourceSet(TestDataSourceSet sourceSet) { | |
| return sourceSet.getBuildDir(); | |
| } | |
| private static List<String> getNamePartsForTestClass(Class<?> clazz) { | |
| List<String> parts = Lists.newArrayList(clazz.getTypeName().split("\\.")); | |
| parts.set(parts.size() - 1, parts.get(parts.size() - 1) + ".class"); | |
| return parts; | |
| } | |
| /** | |
| * Retrieves the source file for a test class from the classpath resources. | |
| * | |
| * <p>This method copies the resource to a temporary file (preserving the original file name) and | |
| * returns its path. | |
| * | |
| * <p>Note: This only works if the corresponding .java file has been explicitly added as a test | |
| * resource in the Gradle build configuration. | |
| */ | |
| public static Path getSourceFileForTestClassFromResources(Class<?> clazz) { | |
| List<String> parts = getNamePartsForTestClass(clazz); | |
| String last = parts.get(parts.size() - 1); | |
| assert last.endsWith(CLASS_EXTENSION); | |
| String javaFileName = | |
| last.substring(0, last.length() - CLASS_EXTENSION.length()) + JAVA_EXTENSION; | |
| return getResourceAsReadOnlyFile(clazz, javaFileName); | |
| } | |
| /** | |
| * Retrieves a resource from the classpath as a {@link Path}. | |
| * | |
| * <p>If the resource is a file on the local filesystem, the path to the resource is returned | |
| * directly to avoid copying. Otherwise, the resource is copied to a temporary file. | |
| * | |
| * <p>Note: This only works if the corresponding file has been explicitly added as a test resource | |
| * in the Gradle build configuration. '.class' files are always available but might be obfuscated. | |
| * | |
| * <p>If the resource name is absolute (starts with "/") then the given class doesn't matter. If | |
| * the resource name is relative then its resolved based on the package of the given class, e.g. | |
| * (A.B.C.class, "foo/bar.txt") finds "A/B/foo/bar.txt". | |
| */ | |
| public static Path getResourceAsReadOnlyFile(Class<?> clazz, String resourceName) { | |
| java.net.URL resourceUrl = clazz.getResource(resourceName); | |
| if (resourceUrl == null) { | |
| throw new RuntimeException( | |
| "Could not find resource " + resourceName + " relative to " + clazz); | |
| } | |
| if ("file".equals(resourceUrl.getProtocol())) { | |
| try { | |
| return Paths.get(resourceUrl.toURI()); | |
| } catch (java.net.URISyntaxException e) { | |
| // Fallback to copying | |
| } | |
| } | |
| String fileName = resourceName; | |
| int lastSlash = fileName.lastIndexOf('/'); | |
| if (lastSlash != -1) { | |
| fileName = fileName.substring(lastSlash + 1); | |
| } | |
| try { | |
| return copyResourceToTempFile(resourceUrl, fileName); | |
| } catch (IOException e) { | |
| throw new RuntimeException( | |
| "Failed to copy resource " | |
| + resourceName | |
| + " relative to " | |
| + clazz | |
| + " to temp file", | |
| e); | |
| } | |
| } | |
| private static Path copyResourceToTempFile(java.net.URL resourceUrl, String fileName) | |
| throws IOException { | |
| Path base = getR8TempPath(); | |
| Files.createDirectories(base); | |
| Path tempDir = Files.createTempDirectory(base, "source-"); | |
| tempDir.toFile().deleteOnExit(); | |
| Path tempFile = tempDir.resolve(fileName); | |
| tempFile.toFile().deleteOnExit(); | |
| try (InputStream is = resourceUrl.openStream()) { | |
| Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING); | |
| } | |
| return tempFile; | |
| } | |
| public static Path getClassFileForTestClassFromResources(Class<?> clazz) { | |
| String binaryName = clazz.getName(); | |
| int lastDot = binaryName.lastIndexOf('.'); | |
| String classNameWithOuter = lastDot == -1 ? binaryName : binaryName.substring(lastDot + 1); | |
| String resourceName = classNameWithOuter + ".class"; | |
| return getResourceAsReadOnlyFile(clazz, resourceName); | |
| } | |
| public static ZipUtils.ZipBuilder addClassToZipBuilder(ZipUtils.ZipBuilder builder, Class<?> clazz) throws IOException { | |
| String entryName = ZipUtils.zipEntryNameForClass(clazz); | |
| Path path = getClassFileForTestClassFromResources(clazz); | |
| return builder.addFile(entryName, path); | |
| } | |
| public static ZipUtils.ZipBuilder addClassToZipBuilder(ZipUtils.ZipBuilder builder, Class<?> anchor, String relativeClassName) throws IOException { | |
| String packagePath = anchor.getPackage().getName().replace('.', '/'); | |
| String entryName = packagePath + "/" + relativeClassName + CLASS_EXTENSION; | |
| Path path = getResourceAsReadOnlyFile(anchor, relativeClassName + CLASS_EXTENSION); | |
| builder.addFile(entryName, path); | |
| return builder; | |
| } | |
| public static Path getClassFileForTestClass(Class<?> clazz) { | |
| return getClassFileForTestClass(clazz, computeLegacyOrGradleSpecifiedLocation()); | |
| } | |
| public static Path getClassFileForTestClass(Class<?> clazz, TestDataSourceSet sourceSet) { | |
| List<String> parts = getNamePartsForTestClass(clazz); | |
| Path filePath = Paths.get("", parts.toArray(StringUtils.EMPTY_ARRAY)); | |
| Path resolveTestPath = getClassPathForTestDataSourceSet(sourceSet).resolve(filePath); | |
| Path resolveBasePath = sourceSet.getTestBaseClassLocation().resolve(filePath); | |
| boolean foundTestPath = Files.exists(resolveTestPath); | |
| boolean foundBasePath = Files.exists(resolveBasePath); | |
| if (resolveTestPath.equals(resolveBasePath)) { | |
| throw new RuntimeException( | |
| "Unexpected configuration with identical test and test-base path" + resolveTestPath); | |
| } | |
| if (foundTestPath && foundBasePath) { | |
| if (identicalClassesIgnoringClassFileVersion(resolveTestPath, resolveBasePath)) { | |
| System.out.println( | |
| "Ambiguous yet identical test file: " + resolveTestPath + " and " + resolveBasePath); | |
| return resolveBasePath; | |
| } | |
| throw new RuntimeException( | |
| "Ambiguous test file: " + resolveTestPath + " and " + resolveBasePath); | |
| } | |
| if (foundTestPath) { | |
| return resolveTestPath; | |
| } | |
| if (foundBasePath) { | |
| return resolveBasePath; | |
| } | |
| // Fallback for assistant module test dependencies during CF generation | |
| Path assistantPath = | |
| Paths.get(getProjectRoot(), "d8_r8", "assistant", "build", "classes", "java", "main") | |
| .resolve(filePath); | |
| if (Files.exists(assistantPath)) { | |
| return assistantPath; | |
| } | |
| throw new RuntimeException( | |
| "Could not find test class in either of paths:\n" | |
| + resolveTestPath | |
| + " and\n" | |
| + resolveBasePath); | |
| } | |
| private static boolean identicalClassesIgnoringClassFileVersion( | |
| Path resolveBasePath, Path resolveTestPath) { | |
| InternalOptions options = new InternalOptions(); | |
| try { | |
| AndroidApp input1 = AndroidApp.builder().addProgramFiles(resolveBasePath).build(); | |
| DexApplication app1 = new ApplicationReader(input1, options, Timing.empty()).read(); | |
| DexProgramClass clazz1 = app1.classes().iterator().next(); | |
| clazz1.downgradeInitialClassFileVersion(CfVersion.V1_8); | |
| AndroidApp input2 = AndroidApp.builder().addProgramFiles(resolveTestPath).build(); | |
| DexApplication app2 = new ApplicationReader(input2, options, Timing.empty()).read(); | |
| DexProgramClass clazz2 = app2.classes().iterator().next(); | |
| clazz2.downgradeInitialClassFileVersion(CfVersion.V1_8); | |
| return clazz1.compareTo(clazz2) == 0; | |
| } catch (Exception e) { | |
| return false; | |
| } | |
| } | |
| public static Collection<Path> getClassFilesForInnerClasses(Collection<Class<?>> classes) | |
| throws IOException { | |
| return getClassFilesForInnerClasses(computeLegacyOrGradleSpecifiedLocation(), classes); | |
| } | |
| public static Collection<Path> getClassFilesForInnerClasses(Class<?>... classes) | |
| throws IOException { | |
| return getClassFilesForInnerClasses( | |
| computeLegacyOrGradleSpecifiedLocation(), Arrays.asList(classes)); | |
| } | |
| public static Collection<Path> getClassFilesForInnerClasses( | |
| TestDataSourceSet sourceSet, Class<?>... classes) throws IOException { | |
| return getClassFilesForInnerClasses(sourceSet, Arrays.asList(classes)); | |
| } | |
| public static Collection<Path> getClassFilesForInnerClasses( | |
| TestDataSourceSet sourceSet, Collection<Class<?>> classes) throws IOException { | |
| Set<Path> paths = new HashSet<>(); | |
| for (Class<?> clazz : classes) { | |
| Path path = ToolHelper.getClassFileForTestClass(clazz, sourceSet); | |
| String prefix = path.toString().replace(CLASS_EXTENSION, "$"); | |
| try (Stream<Path> walk = Files.walk(path.getParent())) { | |
| walk.filter(p -> p.toString().endsWith(CLASS_EXTENSION) && p.toString().startsWith(prefix)) | |
| .forEach(paths::add); | |
| } | |
| } | |
| return paths; | |
| } | |
| public static Path getFileNameForTestClass(Class<?> clazz) { | |
| List<String> parts = getNamePartsForTestClass(clazz); | |
| return Paths.get("", parts.toArray(StringUtils.EMPTY_ARRAY)); | |
| } | |
| public static String getJarEntryForTestClass(Class<?> clazz) { | |
| List<String> parts = getNamePartsForTestClass(clazz); | |
| return String.join("/", parts); | |
| } | |
| public static DirectMappedDexApplication buildApplication(List<String> fileNames) | |
| throws IOException, ExecutionException { | |
| return buildApplicationWithAndroidJar(fileNames, getDefaultAndroidJar()); | |
| } | |
| public static DirectMappedDexApplication buildApplicationWithAndroidJar( | |
| List<String> fileNames, Path androidJar) throws IOException { | |
| AndroidApp input = | |
| AndroidApp.builder() | |
| .addProgramFiles(ListUtils.map(fileNames, Paths::get)) | |
| .addLibraryFiles(androidJar) | |
| .build(); | |
| return new ApplicationReader(input, new InternalOptions(), Timing.empty()) | |
| .readDirectSingleThreaded(); | |
| } | |
| public static ProguardConfiguration loadProguardConfiguration( | |
| DexItemFactory factory, List<Path> configPaths) { | |
| Reporter reporter = new Reporter(); | |
| assert !configPaths.isEmpty(); | |
| ProguardConfiguration.Builder builder = ProguardConfiguration.builder(factory, reporter); | |
| ProguardConfigurationParser parser = | |
| new ProguardConfigurationParser(factory, reporter, builder); | |
| for (Path configPath : configPaths) { | |
| parser.parse(configPath); | |
| } | |
| return builder.buildForTesting(); | |
| } | |
| public static D8Command.Builder prepareD8CommandBuilder(AndroidApp app) { | |
| return D8Command.builder(app); | |
| } | |
| public static R8Command.Builder prepareR8CommandBuilder(AndroidApp app) { | |
| return prepareR8CommandBuilder(app, DexIndexedConsumer.emptyConsumer()); | |
| } | |
| public static R8Command.Builder prepareR8CommandBuilder( | |
| AndroidApp app, ProgramConsumer programConsumer) { | |
| return R8Command.builder(app) | |
| .setProgramConsumer(programConsumer) | |
| .setProguardMapConsumer(StringConsumer.emptyConsumer()); | |
| } | |
| public static R8Command.Builder prepareR8CommandBuilder( | |
| AndroidApp app, ProgramConsumer programConsumer, DiagnosticsHandler diagnosticsHandler) { | |
| return R8Command.builder(app, diagnosticsHandler) | |
| .setProgramConsumer(programConsumer) | |
| .setProguardMapConsumer(StringConsumer.emptyConsumer()); | |
| } | |
| public static AndroidApp runR8(AndroidApp app) throws CompilationFailedException { | |
| return runR8WithProgramConsumer(app, DexIndexedConsumer.emptyConsumer()); | |
| } | |
| public static AndroidApp runR8WithProgramConsumer(AndroidApp app, ProgramConsumer programConsumer) | |
| throws CompilationFailedException { | |
| return runR8(prepareR8CommandBuilder(app, programConsumer).build()); | |
| } | |
| public static AndroidApp runR8(AndroidApp app, Consumer<InternalOptions> optionsConsumer) | |
| throws CompilationFailedException { | |
| R8Command command = prepareR8CommandBuilder(app) | |
| .setDisableTreeShaking(true) | |
| .setDisableMinification(true) | |
| .build(); | |
| return runR8(command, optionsConsumer); | |
| } | |
| public static AndroidApp runR8(R8Command command) throws CompilationFailedException { | |
| return runR8(command, null); | |
| } | |
| public static AndroidApp runR8(R8Command command, Consumer<InternalOptions> optionsConsumer) | |
| throws CompilationFailedException { | |
| return runR8WithFullResult(command, optionsConsumer); | |
| } | |
| public static void runR8WithOptionsModificationOnly( | |
| R8Command command, Consumer<InternalOptions> optionsConsumer) | |
| throws CompilationFailedException { | |
| AndroidApp app = command.getInputApp(); | |
| InternalOptions options = command.getInternalOptions(); | |
| optionsConsumer.accept(options); | |
| R8.runForTesting(app, options); | |
| } | |
| public static void runAndBenchmarkR8WithoutResult( | |
| R8Command.Builder commandBuilder, | |
| Consumer<InternalOptions> optionsConsumer, | |
| BenchmarkResults benchmarkResults) | |
| throws CompilationFailedException { | |
| long start = 0; | |
| CaptureGcResult startGcResult = null; | |
| if (benchmarkResults != null) { | |
| if (benchmarkResults.isBenchmarkingGc()) { | |
| // Try to run gc before starting the benchmark so that previous benchmark iterations do not | |
| // inadvertently impact gc during the current iteration. | |
| System.gc(); | |
| System.gc(); | |
| startGcResult = CaptureGcResult.capture(); | |
| } | |
| start = System.nanoTime(); | |
| } | |
| R8Command command = commandBuilder.build(); | |
| InternalOptions internalOptions = command.getInternalOptions(); | |
| optionsConsumer.accept(internalOptions); | |
| try { | |
| R8.runForTesting(command.getInputApp(), internalOptions); | |
| } finally { | |
| if (benchmarkResults != null) { | |
| long end = System.nanoTime(); | |
| long runtimeResult = end - start; | |
| benchmarkResults.addRuntimeResult(runtimeResult); | |
| if (startGcResult != null) { | |
| CaptureGcResult endGcResult = CaptureGcResult.capture(); | |
| CaptureGcResult delta = startGcResult.computeDelta(endGcResult); | |
| benchmarkResults.addGcOldGenCountResult(delta.getOldCount()); | |
| benchmarkResults.addGcOldGenTimeResult(delta.getOldTimeNanos()); | |
| benchmarkResults.addGcYoungGenCountResult(delta.getYoungCount()); | |
| benchmarkResults.addGcYoungGenTimeResult(delta.getYoungTimeNanos()); | |
| } | |
| } | |
| } | |
| } | |
| public static void runAndBenchmarkR8PartialWithoutResult( | |
| R8Command.Builder commandBuilder, | |
| Consumer<InternalOptions> optionsConsumer, | |
| BenchmarkResults benchmarkResults) | |
| throws CompilationFailedException { | |
| long start = 0; | |
| if (benchmarkResults != null) { | |
| start = System.nanoTime(); | |
| } | |
| R8Command command = commandBuilder.build(); | |
| InternalOptions internalOptions = command.getInternalOptions(); | |
| optionsConsumer.accept(internalOptions); | |
| try { | |
| R8Partial.runForTesting(command.getInputApp(), internalOptions); | |
| } finally { | |
| if (benchmarkResults != null) { | |
| long end = System.nanoTime(); | |
| benchmarkResults.addRuntimeResult(end - start); | |
| } | |
| } | |
| } | |
| public static AndroidApp runR8WithFullResult( | |
| R8Command command, Consumer<InternalOptions> optionsConsumer) | |
| throws CompilationFailedException { | |
| // TODO(zerny): Should we really be adding the android library in ToolHelper? | |
| AndroidApp app = command.getInputApp(); | |
| if (app.getLibraryResourceProviders().isEmpty()) { | |
| // Add the android library matching the minsdk. We filter out junit and testing classes | |
| // from the android jar to avoid duplicate classes in art tests. | |
| AndroidApp.Builder builder = AndroidApp.builder(app); | |
| addFilteredAndroidJar( | |
| builder, AndroidApiLevel.getAndroidApiLevel(command.getUncheckedMinApiLevel())); | |
| app = builder.build(); | |
| } | |
| InternalOptions options = command.getInternalOptions(); | |
| if (optionsConsumer != null) { | |
| optionsConsumer.accept(options); | |
| } | |
| AndroidAppConsumers compatSink = new AndroidAppConsumers(options); | |
| R8.runForTesting(app, options); | |
| return compatSink.build(); | |
| } | |
| public static void runL8(L8Command command, Consumer<InternalOptions> optionsModifier) | |
| throws CompilationFailedException { | |
| InternalOptions internalOptions = command.getInternalOptions(); | |
| optionsModifier.accept(internalOptions); | |
| L8.runForTesting( | |
| command.getInputApp(), | |
| internalOptions, | |
| command.isShrinking(), | |
| command.getD8Command(), | |
| command.getR8Command()); | |
| } | |
| public static void addFilteredAndroidJar( | |
| BaseCommand.Builder<?, ?> builder, AndroidApiLevel apiLevel) { | |
| addFilteredAndroidJar(getAppBuilder(builder), apiLevel); | |
| } | |
| public static void addFilteredAndroidJar(AndroidApp.Builder builder, AndroidApiLevel apiLevel) { | |
| builder.addFilteredLibraryArchives( | |
| Collections.singletonList( | |
| new FilteredClassPath( | |
| getAndroidJar(apiLevel), | |
| ImmutableList.of("!junit/**", "!android/test/**"), | |
| Origin.unknown(), | |
| Position.UNKNOWN))); | |
| } | |
| public static AndroidApp runD8(AndroidApp app) throws CompilationFailedException { | |
| return runD8(app, null); | |
| } | |
| public static AndroidApp runD8(AndroidApp app, Consumer<InternalOptions> optionsConsumer) | |
| throws CompilationFailedException { | |
| return runD8(D8Command.builder(app), optionsConsumer); | |
| } | |
| public static AndroidApp runD8(D8Command.Builder builder) throws CompilationFailedException { | |
| return runD8(builder, null); | |
| } | |
| public static AndroidApp runD8( | |
| D8Command.Builder builder, Consumer<InternalOptions> optionsConsumer) | |
| throws CompilationFailedException { | |
| return runAndBenchmarkD8(builder, optionsConsumer, null); | |
| } | |
| public static AndroidApp runAndBenchmarkD8( | |
| D8Command.Builder builder, | |
| Consumer<InternalOptions> optionsConsumer, | |
| BenchmarkResults benchmarkResults) | |
| throws CompilationFailedException { | |
| AndroidAppConsumers compatSink = new AndroidAppConsumers(builder); | |
| long start = 0; | |
| if (benchmarkResults != null) { | |
| start = System.nanoTime(); | |
| } | |
| D8Command command = builder.build(); | |
| InternalOptions options = command.getInternalOptions(); | |
| if (optionsConsumer != null) { | |
| ExceptionUtils.withD8CompilationHandler( | |
| options.reporter, () -> optionsConsumer.accept(options)); | |
| } | |
| D8.runForTesting(command.getInputApp(), options); | |
| if (benchmarkResults != null) { | |
| long end = System.nanoTime(); | |
| benchmarkResults.addRuntimeResult(end - start); | |
| } | |
| return compatSink.build(); | |
| } | |
| public static void runLegacyResourceShrinker( | |
| ResourceShrinker.Builder builder, | |
| ReferenceChecker callback) | |
| throws IOException, CompilationFailedException, ExecutionException { | |
| ResourceShrinker.Command command = builder.build(); | |
| InternalOptions options = command.getInternalOptions(); | |
| ResourceShrinker.runForTesting(command.getInputApp(), options, callback); | |
| } | |
| @Deprecated | |
| public static ProcessResult runJava(Class clazz) throws Exception { | |
| String main = clazz.getTypeName(); | |
| Path path = getClassPathForTests(); | |
| return runJava(path, main); | |
| } | |
| @Deprecated | |
| public static ProcessResult runJava(Path classpath, String... args) throws IOException { | |
| return runJava(ImmutableList.of(classpath), args); | |
| } | |
| @Deprecated | |
| public static ProcessResult runJava(List<Path> classpath, String... args) throws IOException { | |
| return runJava(ImmutableList.of(), classpath, args); | |
| } | |
| private static ProcessResult runJava(List<String> vmArgs, List<Path> classpath, String... args) | |
| throws IOException { | |
| return runJava(TestRuntime.getSystemRuntime().asCf(), vmArgs, classpath, args); | |
| } | |
| public static ProcessResult runJava(CfRuntime runtime, List<Path> classpath, String... args) | |
| throws IOException { | |
| return runJava(runtime, ImmutableList.of(), classpath, args); | |
| } | |
| public static ProcessResult runJava( | |
| CfRuntime runtime, List<String> vmArgs, List<Path> classpath, String... args) | |
| throws IOException { | |
| return runJava(runtime, vmArgs, ImmutableList.of(), classpath, args); | |
| } | |
| public static ProcessResult runJava( | |
| CfRuntime runtime, | |
| List<String> vmArgs, | |
| List<Path> bootClasspaths, | |
| List<Path> classpath, | |
| String... args) | |
| throws IOException { | |
| List<String> cmdline = new ArrayList<>(Arrays.asList(runtime.getJavaExecutable().toString())); | |
| int activeProcessors = Runtime.getRuntime().availableProcessors(); | |
| if (runtime.isNewerThanOrEqual(CfVm.JDK11) | |
| && vmArgs.stream().noneMatch(arg -> arg.startsWith("-XX:ActiveProcessorCount="))) { | |
| cmdline.add("-XX:ActiveProcessorCount=" + activeProcessors); | |
| } | |
| if (vmArgs.stream() | |
| .noneMatch( | |
| arg -> arg.startsWith("-Djava.util.concurrent.ForkJoinPool.common.parallelism="))) { | |
| cmdline.add( | |
| "-Djava.util.concurrent.ForkJoinPool.common.parallelism=" | |
| + Math.max(1, activeProcessors - 1)); | |
| } | |
| cmdline.addAll(vmArgs); | |
| if (!bootClasspaths.isEmpty()) { | |
| cmdline.add( | |
| "-Xbootclasspath/a:" | |
| + bootClasspaths.stream() | |
| .map(Path::toString) | |
| .collect(Collectors.joining(CLASSPATH_SEPARATOR))); | |
| } | |
| cmdline.add("-cp"); | |
| cmdline.add( | |
| classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR))); | |
| cmdline.addAll(Arrays.asList(args)); | |
| ProcessBuilder builder = new ProcessBuilder(cmdline); | |
| return runProcess(builder); | |
| } | |
| public static ProcessResult runJavaNoVerify( | |
| Path classpath, String mainClass, String... args) throws IOException { | |
| return runJavaNoVerify( | |
| Collections.singletonList(classpath), mainClass, Lists.newArrayList(args)); | |
| } | |
| public static ProcessResult runJavaNoVerify( | |
| List<Path> classpath, String mainClass, String... args) throws IOException { | |
| return runJavaNoVerify(classpath, mainClass, Lists.newArrayList(args)); | |
| } | |
| public static ProcessResult runJavaNoVerify( | |
| List<Path> classpath, String mainClass, List<String> args) throws IOException { | |
| String cp = | |
| classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR)); | |
| ArrayList<String> cmdline = Lists.newArrayList( | |
| getJavaExecutable(), "-cp", cp, "-noverify", mainClass); | |
| cmdline.addAll(args); | |
| ProcessBuilder builder = new ProcessBuilder(cmdline); | |
| return runProcess(builder); | |
| } | |
| public static ProcessResult runAapt2(String... args) throws IOException { | |
| ArrayList<String> cmd = Lists.newArrayList(TestDeps.getAapt2().toString()); | |
| cmd.addAll(Lists.newArrayList(args)); | |
| ProcessBuilder builder = new ProcessBuilder(cmd); | |
| return runProcess(builder); | |
| } | |
| public static ProcessResult forkD8(Path dir, String... args) throws IOException { | |
| return forkJava(dir, D8.class, args); | |
| } | |
| public static ProcessResult forkR8(Path dir, String... args) throws IOException { | |
| return forkJava(dir, R8.class, args); | |
| } | |
| public static ProcessResult forkGenerateMainDexList(Path dir, List<String> args1, String... args2) | |
| throws IOException { | |
| List<String> args = new ArrayList<>(); | |
| args.addAll(args1); | |
| args.addAll(Arrays.asList(args2)); | |
| return forkJava(dir, GenerateMainDexList.class, args); | |
| } | |
| private static ProcessResult forkJava(Path dir, Class clazz, String... args) throws IOException { | |
| return forkJava(dir, clazz, Arrays.asList(args)); | |
| } | |
| private static ProcessResult forkJavaWithJar(Path dir, String jarPath, List<String> args) | |
| throws IOException { | |
| return forkJavaWithJarAndJavaOptions(dir, ImmutableList.of(), jarPath, args); | |
| } | |
| private static ProcessResult forkJavaWithJarAndJavaOptions( | |
| Path dir, List<String> javaOptions, String jarPath, List<String> args) throws IOException { | |
| List<String> command = | |
| new ImmutableList.Builder<String>() | |
| .add(getJavaExecutable()) | |
| .addAll(javaOptions) | |
| .add("-jar") | |
| .add(jarPath) | |
| .addAll(args) | |
| .build(); | |
| return runProcess(new ProcessBuilder(command).directory(dir.toFile())); | |
| } | |
| public static ProcessResult forkJavaWithJavaOptions( | |
| Path dir, List<String> javaOptions, Class clazz, List<String> args) throws IOException { | |
| List<String> command = | |
| new ImmutableList.Builder<String>() | |
| .add(getJavaExecutable()) | |
| .addAll(javaOptions) | |
| .add("-cp") | |
| .add(System.getProperty("java.class.path")) | |
| .add(clazz.getCanonicalName()) | |
| .addAll(args) | |
| .build(); | |
| return runProcess(new ProcessBuilder(command).directory(dir.toFile())); | |
| } | |
| private static ProcessResult forkJava(Path dir, Class clazz, List<String> args) | |
| throws IOException { | |
| List<String> command = new ImmutableList.Builder<String>() | |
| .add(getJavaExecutable()) | |
| .add("-cp").add(System.getProperty("java.class.path")) | |
| .add(clazz.getCanonicalName()) | |
| .addAll(args) | |
| .build(); | |
| return runProcess(new ProcessBuilder(command).directory(dir.toFile())); | |
| } | |
| @Deprecated | |
| // Use CfRuntime.getJavaExecutable() for a specific JDK or getSystemJavaExecutable | |
| public static String getJavaExecutable() { | |
| return getSystemJavaExecutable(); | |
| } | |
| public static String getSystemJavaExecutable() { | |
| return TestRuntime.getSystemRuntime().asCf().getJavaExecutable().toString(); | |
| } | |
| public static ProcessResult runArtRaw(ArtCommandBuilder builder) throws IOException { | |
| return runArtProcessRaw(builder); | |
| } | |
| public static ProcessResult runArtRaw(String file, String mainClass, DexVm version) | |
| throws IOException { | |
| return runArtRaw(Collections.singletonList(file), mainClass, null, version, false); | |
| } | |
| // Index used to name directory aimed at storing dex files and process result | |
| // for one invokation of runArtRaw() in order to avoid conflicts in case of | |
| // multiple calls within the same test. | |
| private static int testOutputPathIndex = 0; | |
| public static ProcessResult runArtRaw( | |
| List<String> files, | |
| String mainClass, | |
| Consumer<ArtCommandBuilder> extras, | |
| DexVm version, | |
| boolean withArtFrameworks, | |
| String... args) | |
| throws IOException { | |
| ArtCommandBuilder builder = new ArtCommandBuilder(version); | |
| builder.withArtFrameworks = withArtFrameworks; | |
| files.forEach(builder::appendClasspath); | |
| builder.setMainClass(mainClass); | |
| if (extras != null) { | |
| extras.accept(builder); | |
| } | |
| for (String arg : args) { | |
| builder.appendProgramArgument(arg); | |
| } | |
| ProcessResult processResult = null; | |
| // Whenever we start a new test method we reset the index count. | |
| String reset_output_index = System.getProperty("reset_output_index"); | |
| if (reset_output_index != null) { | |
| System.clearProperty("reset_output_index"); | |
| testOutputPathIndex = 0; | |
| } else { | |
| assert testOutputPathIndex >= 0; | |
| testOutputPathIndex++; | |
| } | |
| String goldenFilesDirInProp = System.getProperty("use_golden_files_in"); | |
| if (goldenFilesDirInProp != null) { | |
| File goldenFileDir = new File(goldenFilesDirInProp); | |
| assert goldenFileDir.isDirectory(); | |
| processResult = | |
| compareAgainstGoldenFiles( | |
| files.stream().map(File::new).collect(Collectors.toList()), goldenFileDir); | |
| if (processResult.exitCode == 0) { | |
| processResult = readProcessResult(goldenFileDir); | |
| } | |
| } else { | |
| processResult = runArtProcessRaw(builder); | |
| } | |
| String goldenFilesDirToProp = System.getProperty("generate_golden_files_to"); | |
| if (goldenFilesDirToProp != null) { | |
| File goldenFileDir = new File(goldenFilesDirToProp); | |
| assert goldenFileDir.isDirectory(); | |
| storeAsGoldenFiles(files.stream().map(File::new).collect(Collectors.toList()), goldenFileDir); | |
| storeProcessResult(processResult, goldenFileDir); | |
| } | |
| return processResult; | |
| } | |
| public static ProcessResult runJaCoCoInstrument(Path sourceClassFiles, Path outputDirectory) | |
| throws IOException { | |
| List<String> cmdline = new ArrayList<>(); | |
| cmdline.add(TestRuntime.getSystemRuntime().asCf().getJavaExecutable().toString()); | |
| cmdline.add("-jar"); | |
| cmdline.add(TestDeps.getJacocoCliJar().toString()); | |
| cmdline.add("instrument"); | |
| cmdline.add(sourceClassFiles.toString()); | |
| cmdline.add("--dest"); | |
| cmdline.add(outputDirectory.toString()); | |
| ProcessBuilder builder = new ProcessBuilder(cmdline); | |
| return ToolHelper.runProcess(builder); | |
| } | |
| public static ProcessResult runJaCoCoReport(Path classfiles, Path jacocoExec, Path reportFile) | |
| throws IOException { | |
| List<String> cmdline = new ArrayList<>(); | |
| cmdline.add(TestRuntime.getSystemRuntime().asCf().getJavaExecutable().toString()); | |
| cmdline.add("-jar"); | |
| cmdline.add(TestDeps.getJacocoCliJar().toString()); | |
| cmdline.add("report"); | |
| cmdline.add(jacocoExec.toString()); | |
| cmdline.add("--classfiles"); | |
| cmdline.add(classfiles.toString()); | |
| cmdline.add("--csv"); | |
| cmdline.add(reportFile.toString()); | |
| ProcessBuilder builder = new ProcessBuilder(cmdline); | |
| return ToolHelper.runProcess(builder); | |
| } | |
| private static Path findNonConflictingDestinationFilePath(Path testOutputPath) { | |
| int index = 0; | |
| Path destFilePath; | |
| do { | |
| destFilePath = Paths.get(testOutputPath.toString(), | |
| "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION); | |
| index++; | |
| } while (destFilePath.toFile().exists()); | |
| return destFilePath; | |
| } | |
| private static Path getTestOutputPath(File destDir) throws IOException { | |
| assert destDir.exists(); | |
| assert destDir.isDirectory(); | |
| String testClassName = System.getProperty("test_class_name"); | |
| String testName = System.getProperty("test_name"); | |
| String headSha1 = System.getProperty("test_git_HEAD_sha1"); | |
| assert testClassName != null; | |
| assert testName != null; | |
| assert headSha1 != null; | |
| return Files.createDirectories( | |
| Paths.get( | |
| destDir.getAbsolutePath(), | |
| headSha1, | |
| testClassName, | |
| testName + "-" + String.format("%03d", testOutputPathIndex))); | |
| } | |
| private static List<File> unzipDexFilesArchive(File zipFile) throws IOException { | |
| Path base = getR8TempPath(); | |
| Files.createDirectories(base); | |
| File tmpDirFile = Files.createTempDirectory(base, "r8-test-").toFile(); | |
| tmpDirFile.deleteOnExit(); | |
| return ZipUtils.unzip(zipFile.getAbsolutePath(), tmpDirFile); | |
| } | |
| private static void storeAsGoldenFiles(List<File> files, File destDir) throws IOException { | |
| Path testOutputPath = getTestOutputPath(destDir); | |
| for (File f : files) { | |
| Path filePath = f.toPath(); | |
| // TODO(jmhenaff): Check it's been produced by D8/R8? | |
| List<File> testFiles = Collections.singletonList(f); | |
| if (FileUtils.isArchive(filePath)) { | |
| testFiles = unzipDexFilesArchive(f); | |
| } | |
| for (File testFile : testFiles) { | |
| Path testFilePath = testFile.toPath(); | |
| if (FileUtils.isDexFile(testFilePath)) { | |
| Path destFile = findNonConflictingDestinationFilePath(testOutputPath); | |
| FileUtils.writeToFile(destFile, null, Files.readAllBytes(testFilePath)); | |
| } | |
| } | |
| } | |
| } | |
| @SuppressWarnings("unchecked") | |
| private static void storeProcessResult(ProcessResult processResult, File dest) | |
| throws IOException { | |
| Gson gson = new Gson(); | |
| Path testOutputPath = getTestOutputPath(dest); | |
| try (FileWriter fw = new FileWriter(new File(testOutputPath.toFile(), "processResult.json"))) { | |
| gson.toJson(processResult, ProcessResult.class, fw); | |
| } | |
| } | |
| private static ProcessResult readProcessResult(File dest) throws IOException { | |
| File processResultFile = new File(getTestOutputPath(dest).toFile(), "processResult.json"); | |
| Gson gson = new Gson(); | |
| try (FileReader fr = new FileReader(processResultFile)) { | |
| return gson.fromJson(fr, ProcessResult.class); | |
| } | |
| } | |
| private static ProcessResult compareAgainstGoldenFiles(List<File> files, File destDir) | |
| throws IOException { | |
| Path testOutputPath = getTestOutputPath(destDir); | |
| int index = 0; | |
| String stdErr = ""; | |
| boolean passed = true; | |
| for (File f : files) { | |
| Path filePath = f.toPath(); | |
| List<File> testFiles = Collections.singletonList(f); | |
| if (FileUtils.isArchive(filePath)) { | |
| testFiles = unzipDexFilesArchive(f); | |
| } | |
| for (File testFile : testFiles) { | |
| Path testFilePath = testFile.toPath(); | |
| // TODO(jmhenaff): Check it's been produced by D8/R8? | |
| if (FileUtils.isDexFile(testFilePath)) { | |
| File goldenFile = Paths.get(testOutputPath.toString(), | |
| "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile(); | |
| if (!goldenFile.exists()) { | |
| String fileDesc = "'" + testFile.getAbsolutePath() + "'"; | |
| if (FileUtils.isZipFile(filePath)) { | |
| fileDesc += " (extracted from '" + f.getAbsolutePath() + "')"; | |
| } | |
| stdErr += "Cannot find golden file '" + goldenFile.getAbsolutePath() | |
| + "' to compare against test file " + fileDesc + "\n"; | |
| passed = false; | |
| } else if (!com.google.common.io.Files.equal(testFile, goldenFile)) { | |
| String fileDesc = "'" + testFile.getAbsolutePath() + "'"; | |
| if (FileUtils.isZipFile(filePath)) { | |
| fileDesc += " (extracted from '" + f.getAbsolutePath() + "')"; | |
| } | |
| stdErr += | |
| "File " + fileDesc + " differs from golden file '" + goldenFile.getAbsolutePath() | |
| + "'\n"; | |
| passed = false; | |
| } | |
| index++; | |
| } | |
| } | |
| } | |
| // Ensure we processed as many files as there are golden files | |
| File goldenFile = Paths.get(testOutputPath.toString(), | |
| "classes-" + String.format("%03d", index) + FileUtils.DEX_EXTENSION).toFile(); | |
| if (goldenFile.exists()) { | |
| stdErr += "Less dex files have been produced: there is at least one more golden file ('" | |
| + goldenFile.getAbsolutePath() + "'\n"; | |
| passed = false; | |
| } | |
| return new ProcessResult(passed ? 0 : -1, "", stdErr); | |
| } | |
| public static boolean dealsWithGoldenFiles() { | |
| return compareAgaintsGoldenFiles() || generateGoldenFiles(); | |
| } | |
| public static boolean compareAgaintsGoldenFiles() { | |
| return System.getProperty("use_golden_files_in") != null; | |
| } | |
| public static boolean generateGoldenFiles() { | |
| return System.getProperty("generate_golden_files_to") != null; | |
| } | |
| public static ProcessResult runArtNoVerificationErrorsRaw( | |
| String file, String mainClass, DexVm version) throws IOException { | |
| return runArtNoVerificationErrorsRaw(Collections.singletonList(file), mainClass, null, version); | |
| } | |
| public static ProcessResult runArtNoVerificationErrorsRaw(List<String> files, String mainClass, | |
| Consumer<ArtCommandBuilder> extras, | |
| DexVm version) | |
| throws IOException { | |
| ProcessResult result = runArtRaw(files, mainClass, extras, version, false); | |
| failOnProcessFailure(result); | |
| failOnVerificationErrors(result); | |
| return result; | |
| } | |
| public static String runArtNoVerificationErrors(String file, String mainClass, DexVm version) | |
| throws IOException { | |
| return runArtNoVerificationErrorsRaw(file, mainClass, version).stdout; | |
| } | |
| public static String runArtNoVerificationErrors(List<String> files, String mainClass, | |
| Consumer<ArtCommandBuilder> extras, | |
| DexVm version) | |
| throws IOException { | |
| return runArtNoVerificationErrorsRaw(files, mainClass, extras, version).stdout; | |
| } | |
| protected static void failOnProcessFailure(ProcessResult result) { | |
| if (result.exitCode != 0) { | |
| fail("Unexpected failure: '" + result.stderr + "'\n" + result.stdout); | |
| } | |
| } | |
| protected static void failOnVerificationErrors(ProcessResult result) { | |
| if (result.stderr.contains("Verification error")) { | |
| fail("Verification error: \n" + result.stderr); | |
| } | |
| } | |
| private static final long ART_PROCESS_TIMEOUT_MINUTES = 10; | |
| private static ProcessResult runArtProcessRaw(ArtCommandBuilder builder) throws IOException { | |
| Assume.assumeTrue(artSupported() || dealsWithGoldenFiles()); | |
| ProcessResult cachedResult = builder.getCachedResults(); | |
| if (cachedResult != null) { | |
| return cachedResult; | |
| } | |
| ProcessResult result; | |
| if (builder.isForDevice()) { | |
| try { | |
| result = builder.asDeviceRunner().run(); | |
| } catch (DeviceRunnerConfigurationException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } else { | |
| result = | |
| runProcess( | |
| builder.asProcessBuilder(), | |
| System.out, | |
| ART_PROCESS_TIMEOUT_MINUTES, | |
| TimeUnit.MINUTES); | |
| } | |
| builder.cacheResult(result); | |
| return result; | |
| } | |
| public static String runArt(ArtCommandBuilder builder) throws IOException { | |
| ProcessResult result = runArtProcessRaw(builder); | |
| failOnProcessFailure(result); | |
| return result.stdout; | |
| } | |
| public static String checkArtOutputIdentical(List<String> files1, List<String> files2, | |
| String mainClass, | |
| Consumer<ArtCommandBuilder> extras, | |
| DexVm version) | |
| throws IOException { | |
| return checkArtOutputIdentical( | |
| version, | |
| mainClass, | |
| extras, | |
| ImmutableList.of(ListUtils.map(files1, Paths::get), ListUtils.map(files2, Paths::get))); | |
| } | |
| public static String checkArtOutputIdentical( | |
| DexVm version, | |
| String mainClass, | |
| Consumer<ArtCommandBuilder> extras, | |
| Collection<Collection<Path>> programs) | |
| throws IOException { | |
| for (Collection<Path> program : programs) { | |
| for (Path path : program) { | |
| assertTrue("File " + path + " must exist", Files.exists(path)); | |
| } | |
| } | |
| String output = null; | |
| for (Collection<Path> program : programs) { | |
| String result = | |
| ToolHelper.runArtNoVerificationErrors( | |
| ListUtils.map(program, Path::toString), mainClass, extras, version); | |
| if (output != null) { | |
| assertEquals(output, result); | |
| } else { | |
| output = result; | |
| } | |
| } | |
| return output; | |
| } | |
| public static void runDex2Oat(Path dexFile, Path oatFile, Path temp, DexVm vm) | |
| throws IOException { | |
| ProcessResult result = runDex2OatRaw(dexFile, oatFile, temp, vm); | |
| if (result.exitCode != 0) { | |
| fail("dex2oat failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr); | |
| } | |
| if (result.stderr != null && result.stderr.contains("Verification error")) { | |
| fail("Verification error: \n" + result.stderr); | |
| } | |
| } | |
| // Checked in VMs for which dex2oat should work specified in decreasing order. | |
| private static final List<DexVm> SUPPORTED_DEX2OAT_VMS = | |
| ImmutableList.of( | |
| DexVm.ART_17_2_0_HOST, | |
| DexVm.ART_17_1_0_HOST, | |
| DexVm.ART_17_0_0_HOST, | |
| DexVm.ART_16_0_0_HOST, | |
| DexVm.ART_15_0_0_HOST, | |
| DexVm.ART_13_0_0_HOST, | |
| DexVm.ART_12_0_0_HOST, | |
| DexVm.ART_6_0_1_HOST); | |
| public static boolean isDex2OatSupportedForVM(DexVm vm) { | |
| return SUPPORTED_DEX2OAT_VMS.contains(vm); | |
| } | |
| public static ProcessResult runDex2OatRaw(Path dexFile, Path oatFile, Path temp, DexVm targetVm) | |
| throws IOException { | |
| Assume.assumeTrue(ToolHelper.isDex2OatSupported()); | |
| assert Files.exists(dexFile); | |
| assert ByteStreams.toByteArray(Files.newInputStream(dexFile)).length > 0; | |
| assert SUPPORTED_DEX2OAT_VMS.stream() | |
| .sorted(Comparator.comparing(DexVm::getVersion).reversed()) | |
| .collect(Collectors.toList()) | |
| .equals(SUPPORTED_DEX2OAT_VMS); | |
| DexVersion requiredDexFileVersion = getDexFileVersionForVm(targetVm); | |
| DexVm vm = null; | |
| for (DexVm supported : SUPPORTED_DEX2OAT_VMS) { | |
| DexVersion supportedDexFileVersion = getDexFileVersionForVm(supported); | |
| // This and remaining VMs can't compile code at the required DEX version. | |
| if (supportedDexFileVersion.isLessThan(requiredDexFileVersion)) { | |
| break; | |
| } | |
| // Find the "oldest" supported VM. Only consider VMs older than targetVm if no VM matched. | |
| if (supported.isNewerThanOrEqual(targetVm) || vm == null) { | |
| vm = supported; | |
| } | |
| } | |
| if (vm == null) { | |
| throw new Unimplemented("Unable to find a supported dex2oat for VM " + vm); | |
| } | |
| if (vm.version.isNewerThanOrEqual(DexVm.Version.V13_0_0)) { | |
| return runDex2OatV13AndNewer(dexFile, oatFile, temp, vm); | |
| } | |
| List<String> command = new ArrayList<>(); | |
| command.add(getDex2OatPath(vm).toAbsolutePath().toString()); | |
| command.add("--android-root=" + getProductPath(vm).toAbsolutePath() + "/system"); | |
| command.add("--runtime-arg"); | |
| command.add("-verbose:verifier"); | |
| command.add("--runtime-arg"); | |
| command.add("-Xnorelocate"); | |
| command.add("--dex-file=" + dexFile.toAbsolutePath()); | |
| command.add("--oat-file=" + oatFile.toAbsolutePath()); | |
| command.add("--instruction-set=" + getArchString(vm)); | |
| if (vm.version.equals(DexVm.Version.V12_0_0)) { | |
| command.add( | |
| "--boot-image=" | |
| + getDexVmPath(vm).toAbsolutePath() | |
| + "/apex/art_boot_images/javalib/boot.art"); | |
| } | |
| ProcessBuilder builder = new ProcessBuilder(command); | |
| builder.directory(getDexVmPath(vm).toFile()); | |
| builder.environment().put("LD_LIBRARY_PATH", getDexVmLibPath(vm).toString()); | |
| return runProcess(builder); | |
| } | |
| private static ProcessResult runDex2OatV13AndNewer( | |
| Path dexFile, Path oatFile, Path temp, DexVm vm) throws IOException { | |
| String versionString; | |
| if (vm.version == DexVm.Version.V13_0_0) { | |
| versionString = "33.10"; | |
| } else if (vm.version == DexVm.Version.V15_0_0) { | |
| versionString = "35.14"; | |
| } else if (vm.version == DexVm.Version.V16_0_0) { | |
| versionString = "36.0"; | |
| } else if (vm.version == DexVm.Version.V17_0_0 | |
| || vm.version == DexVm.Version.V17_1_0 | |
| || vm.version == DexVm.Version.V17_2_0) { | |
| versionString = "head"; | |
| } else { | |
| throw new Unreachable(); | |
| } | |
| Path dex2oatDir = Paths.get(THIRD_PARTY_DIR, "dex2oat", versionString); | |
| List<String> command = new ArrayList<>(); | |
| command.add( | |
| dex2oatDir | |
| .resolve("x86_64") | |
| .resolve("bin") | |
| .resolve("dex2oat64") | |
| .toAbsolutePath() | |
| .toString()); | |
| command.add("--android-root=" + dex2oatDir.toAbsolutePath()); | |
| command.add("--generate-debug-info"); | |
| command.add("--dex-location=/system/framework/classes.dex"); | |
| command.add("--dex-file=" + dexFile.toAbsolutePath()); | |
| command.add("--copy-dex-files=always"); | |
| if (!versionString.equals("33.10")) { | |
| command.add("--runtime-arg"); | |
| command.add("-Xgc:CMC"); | |
| } | |
| String[] bootJars = { | |
| "core-oj.jar", "core-libart.jar", "okhttp.jar", "bouncycastle.jar", "apache-xml.jar" | |
| }; | |
| StringBuilder bootClassPath = new StringBuilder("-Xbootclasspath:"); | |
| for (int i = 0; i < bootJars.length; i++) { | |
| if (i > 0) { | |
| bootClassPath.append(":"); | |
| } | |
| bootClassPath.append(dex2oatDir.resolve("bootjars").resolve(bootJars[i]).toAbsolutePath()); | |
| } | |
| command.add("--runtime-arg"); | |
| command.add(bootClassPath.toString()); | |
| command.add("--runtime-arg"); | |
| command.add( | |
| "-Xbootclasspath-locations:/apex/com.android.art/javalib/core-oj.jar" | |
| + ":/apex/com.android.art/javalib/core-libart.jar" | |
| + ":/apex/com.android.art/javalib/okhttp.jar" | |
| + ":/apex/com.android.art/javalib/bouncycastle.jar" | |
| + ":/apex/com.android.art/javalib/apache-xml.jar"); | |
| command.add( | |
| "--boot-image=" | |
| + dex2oatDir | |
| .resolve("app") | |
| .resolve("system") | |
| .resolve("framework") | |
| .resolve("boot.art") | |
| .toAbsolutePath()); | |
| command.add("--oat-file=" + oatFile.toAbsolutePath()); | |
| command.add("--app-image-file=" + temp.resolve("classes.art").toAbsolutePath()); | |
| if (!versionString.equals("33.10")) { | |
| command.add("--force-allow-oj-inlines"); | |
| } | |
| command.add("--instruction-set=" + getArchString(vm)); | |
| ProcessBuilder builder = new ProcessBuilder(command); | |
| return runProcess(builder); | |
| } | |
| public static ProcessResult runProguardRaw( | |
| String proguardScript, Path inJar, Path outJar, List<Path> configs, Path map) | |
| throws IOException { | |
| return runProguardRaw( | |
| proguardScript, inJar, outJar, ToolHelper.getDefaultAndroidJar(), configs, map); | |
| } | |
| public static ProcessResult runProguardRaw( | |
| String proguardScript, Path inJar, Path outJar, Path lib, List<Path> configs, Path map) | |
| throws IOException { | |
| List<String> command = new ArrayList<>(); | |
| command.add(proguardScript); | |
| command.add("-forceprocessing"); // Proguard just checks the creation time on the in/out jars. | |
| command.add("-injars"); | |
| command.add(inJar.toString()); | |
| command.add("-libraryjars"); | |
| command.add(lib.toString()); | |
| configs.forEach(config -> command.add("@" + config)); | |
| command.add("-outjar"); | |
| command.add(outJar.toString()); | |
| command.add("-printmapping"); | |
| if (map != null) { | |
| command.add(map.toString()); | |
| } | |
| ProcessBuilder builder = new ProcessBuilder(command); | |
| return ToolHelper.runProcess(builder); | |
| } | |
| public static String runProguard( | |
| String proguardScript, Path inJar, Path outJar, List<Path> configs, Path map) | |
| throws IOException { | |
| ToolHelper.ProcessResult result = runProguardRaw(proguardScript, inJar, outJar, configs, map); | |
| if (result.exitCode != 0) { | |
| fail("Proguard failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr); | |
| } | |
| return result.stdout; | |
| } | |
| public static ProcessResult runRetraceRaw(Path retracePath, Path map, Path stackTrace) | |
| throws IOException { | |
| List<String> command = new ArrayList<>(); | |
| command.add(retracePath.toString()); | |
| command.add(map.toString()); | |
| command.add(stackTrace.toString()); | |
| ProcessBuilder builder = new ProcessBuilder(command); | |
| return ToolHelper.runProcess(builder); | |
| } | |
| public static String runRetrace(ProguardVersion pgRetracer, Path map, Path stackTrace) | |
| throws IOException { | |
| ProcessResult result = runRetraceRaw(pgRetracer.getRetraceScript(), map, stackTrace); | |
| if (result.exitCode != 0) { | |
| fail("Retrace failed, exit code " + result.exitCode + ", stderr:\n" + result.stderr); | |
| } | |
| return result.stdout; | |
| } | |
| public static class ProcessResult { | |
| public final int exitCode; | |
| public String stdout; | |
| public final String stderr; | |
| public final String command; | |
| public ProcessResult(int exitCode, String stdout, String stderr, String command) { | |
| this.exitCode = exitCode; | |
| this.stdout = stdout; | |
| this.stderr = stderr; | |
| this.command = command; | |
| } | |
| ProcessResult(int exitCode, String stdout, String stderr) { | |
| this(exitCode, stdout, stderr, null); | |
| } | |
| public void setStdout(String stdout) { | |
| this.stdout = stdout; | |
| } | |
| @Override | |
| public String toString() { | |
| StringBuilder builder = new StringBuilder(); | |
| builder.append("EXIT CODE: "); | |
| builder.append(exitCode); | |
| builder.append("\n"); | |
| builder.append("STDOUT: "); | |
| builder.append("\n"); | |
| builder.append( | |
| stdout.length() > 50000 ? "<omitted " + stdout.length() + " characters>" : stdout); | |
| builder.append("\n"); | |
| builder.append("STDERR: "); | |
| builder.append("\n"); | |
| builder.append( | |
| stderr.length() > 50000 ? "<omitted " + stderr.length() + " characters>" : stderr); | |
| builder.append("\n"); | |
| return builder.toString(); | |
| } | |
| } | |
| // Process.pid() is added in Java 9. Until we use Java 9 this can be used on Linux and Mac OS. | |
| // https://docs.oracle.com/javase/9/docs/api/java/lang/Process.html#pid-- | |
| private static synchronized long getPidOfProcess(Process p) { | |
| long pid = -1; | |
| try { | |
| if (p.getClass().getName().equals("java.lang.UNIXProcess")) { | |
| Field f = p.getClass().getDeclaredField("pid"); | |
| f.setAccessible(true); | |
| pid = f.getLong(p); | |
| f.setAccessible(false); | |
| } | |
| } catch (Exception e) { | |
| pid = -1; | |
| } | |
| return pid; | |
| } | |
| public static ProcessResult runProcess(ProcessBuilder builder) throws IOException { | |
| return runProcess(builder, System.out); | |
| } | |
| public static ProcessResult runProcess(ProcessBuilder builder, PrintStream out) | |
| throws IOException { | |
| return runProcess(builder, out, -1, null); | |
| } | |
| public static ProcessResult runProcess( | |
| ProcessBuilder builder, PrintStream out, long timeout, TimeUnit unit) throws IOException { | |
| boolean printCwd = builder.directory() != null; | |
| if (printCwd) { | |
| out.println("(cd " + builder.directory() + "; "); | |
| } | |
| String command = String.join(" ", builder.command()); | |
| out.println(command); | |
| if (printCwd) { | |
| out.println(")"); | |
| } | |
| return drainProcessOutputStreams(builder.start(), command, timeout, unit); | |
| } | |
| public static ProcessResult drainProcessOutputStreams(Process process, String command) { | |
| return drainProcessOutputStreams(process, command, -1, null); | |
| } | |
| public static ProcessResult drainProcessOutputStreams( | |
| Process process, String command, long timeout, TimeUnit unit) { | |
| // Drain stdout and stderr so that the process does not block. Read stdout and stderr | |
| // in parallel to make sure that neither buffer can get filled up which will cause the | |
| // C program to block in a call to write. | |
| StreamReader stdoutReader = new StreamReader(process.getInputStream()); | |
| StreamReader stderrReader = new StreamReader(process.getErrorStream()); | |
| Thread stdoutThread = new Thread(stdoutReader); | |
| Thread stderrThread = new Thread(stderrReader); | |
| stdoutThread.start(); | |
| stderrThread.start(); | |
| try { | |
| if (timeout > 0 && unit != null) { | |
| if (!process.waitFor(timeout, unit)) { | |
| destroyProcess(process); | |
| stdoutThread.join(TimeUnit.SECONDS.toMillis(5)); | |
| stderrThread.join(TimeUnit.SECONDS.toMillis(5)); | |
| throw new RuntimeException( | |
| "Process timed out after " | |
| + timeout | |
| + " " | |
| + StringUtils.toLowerCase(unit.name()) | |
| + ": " | |
| + command | |
| + "\nSTDOUT:\n" | |
| + stdoutReader.getResult() | |
| + "\nSTDERR:\n" | |
| + stderrReader.getResult()); | |
| } | |
| } else { | |
| process.waitFor(); | |
| } | |
| stdoutThread.join(); | |
| stderrThread.join(); | |
| } catch (InterruptedException e) { | |
| throw new RuntimeException("Execution interrupted", e); | |
| } | |
| return new ProcessResult( | |
| process.exitValue(), stdoutReader.getResult(), stderrReader.getResult(), command); | |
| } | |
| private static void destroyProcess(Process process) throws InterruptedException { | |
| List<ProcessHandle> descendants = process.descendants().collect(Collectors.toList()); | |
| if (!descendants.isEmpty()) { | |
| descendants.forEach(ProcessHandle::destroyForcibly); | |
| if (process.waitFor(5, TimeUnit.SECONDS)) { | |
| return; | |
| } | |
| } | |
| process.destroyForcibly(); | |
| process.waitFor(5, TimeUnit.SECONDS); | |
| process.descendants().forEach(ProcessHandle::destroyForcibly); | |
| } | |
| public static R8Command.Builder addProguardConfigurationConsumer( | |
| R8Command.Builder builder, Consumer<ProguardConfiguration.Builder> consumer) { | |
| builder.addProguardConfigurationConsumerForTesting(consumer); | |
| return builder; | |
| } | |
| public static R8Command.Builder addSyntheticProguardRulesConsumerForTesting( | |
| R8Command.Builder builder, Consumer<List<ProguardConfigurationRule>> consumer) { | |
| builder.addSyntheticProguardRulesConsumerForTesting(consumer); | |
| return builder; | |
| } | |
| public static R8Command.Builder allowTestProguardOptions(R8Command.Builder builder) { | |
| builder.setEnableTestProguardOptions(); | |
| return builder; | |
| } | |
| public static AndroidApp getApp(BaseCommand command) { | |
| return command.getInputApp(); | |
| } | |
| public static AndroidApp.Builder getAppBuilder(BaseCommand.Builder builder) { | |
| return builder.getAppBuilder(); | |
| } | |
| public static AndroidApp.Builder builderFromProgramDirectory(Path directory) throws IOException { | |
| AndroidApp.Builder builder = AndroidApp.builder(); | |
| Files.walkFileTree( | |
| directory, | |
| new SimpleFileVisitor<Path>() { | |
| @Override | |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { | |
| if (isDexFile(file)) { | |
| builder.addProgramFile(file); | |
| } | |
| return FileVisitResult.CONTINUE; | |
| } | |
| }); | |
| return builder; | |
| } | |
| public static void writeApplication(AppView<?> appView, Timing timing) throws ExecutionException { | |
| appView.options().tool = Tool.R8; | |
| R8.writeApplication(appView, Executors.newSingleThreadExecutor(), timing); | |
| } | |
| public static void disassemble(AndroidApp app, PrintStream ps) | |
| throws ExecutionException, IOException { | |
| LazyLoadedDexApplication application = | |
| new ApplicationReader(app, new InternalOptions(), Timing.empty()).read(); | |
| new AssemblyWriter(application, new InternalOptions(), true, false, true).write(ps); | |
| } | |
| public static Path getTestFolderForClass(Class<?> clazz) { | |
| Path file = ToolHelper.getFileNameForTestClass(clazz); | |
| Path javaDir = Paths.get(ToolHelper.TESTS_DIR).resolve("java").resolve(file).getParent(); | |
| if (Files.exists(javaDir)) { | |
| return javaDir; | |
| } | |
| Path java8KeepAnnoDir = | |
| Paths.get(ToolHelper.TESTS_DIR).resolve("java8/keepanno").resolve(file).getParent(); | |
| if (Files.exists(java8KeepAnnoDir)) { | |
| return java8KeepAnnoDir; | |
| } | |
| Path java8KotlinDir = | |
| Paths.get(ToolHelper.TESTS_DIR).resolve("java8/kotlin").resolve(file).getParent(); | |
| if (Files.exists(java8KotlinDir)) { | |
| return java8KotlinDir; | |
| } | |
| return javaDir; | |
| } | |
| public static Collection<Path> getFilesInTestFolderRelativeToClass( | |
| Class<?> clazz, String folderName, String... endsWith) throws IOException { | |
| Path subFolder = getTestFolderForClass(clazz).resolve(folderName); | |
| assert Files.isDirectory(subFolder); | |
| List<Path> result = new ArrayList<>(); | |
| for (String s : endsWith) { | |
| try (Stream<Path> walker = Files.walk(subFolder)) { | |
| walker.filter(path -> path.toString().endsWith(s)).forEach(result::add); | |
| } | |
| } | |
| return result; | |
| } | |
| /** This code only works if run with depot_tools on the path */ | |
| public static String uploadFileToGoogleCloudStorage(String bucket, Path file) throws IOException { | |
| ImmutableList.Builder<String> command = new ImmutableList.Builder<>(); | |
| if (isWindows()) { | |
| command.add("python3"); | |
| } | |
| command | |
| .add("upload_to_google_storage.py") | |
| .add("-f") | |
| .add("--bucket") | |
| .add(bucket) | |
| .add(file.toAbsolutePath().toString()); | |
| ProcessResult result = ToolHelper.runProcess(new ProcessBuilder(command.build())); | |
| if (result.exitCode != 0) { | |
| throw new RuntimeException( | |
| "Could not upload " | |
| + file | |
| + " to cloud storage:\n" | |
| + result.stdout | |
| + "\n" | |
| + result.stderr); | |
| } | |
| // Upload will add a sha1 file at the same location. | |
| Path sha1file = file.resolveSibling(file.getFileName() + ".sha1"); | |
| assert Files.exists(sha1file) : sha1file.toString(); | |
| List<String> strings = Files.readAllLines(sha1file); | |
| assert !strings.isEmpty(); | |
| return strings.get(0); | |
| } | |
| public static void setReadEmbeddedRulesFromClasspathAndLibrary( | |
| R8Command.Builder builder, boolean enabled) { | |
| builder.setReadEmbeddedRulesFromClasspathAndLibrary(enabled); | |
| } | |
| } |