/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.modules.targetprep; import static android.compat.testing.Classpaths.ClasspathType.BOOTCLASSPATH; import static android.compat.testing.Classpaths.ClasspathType.SYSTEMSERVERCLASSPATH; import android.compat.testing.Classpaths; import android.compat.testing.Classpaths.ClasspathType; import com.android.modules.proto.ClasspathClasses.Classpath; import com.android.modules.proto.ClasspathClasses.ClasspathClassesDump; import com.android.modules.proto.ClasspathClasses.ClasspathEntry; import com.android.modules.proto.ClasspathClasses.Jar; import com.android.tradefed.config.Option; import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.INativeDevice; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.invoker.TestInformation; import com.android.tradefed.log.LogUtil.CLog; import com.android.tradefed.targetprep.BaseTargetPreparer; import com.android.tradefed.targetprep.TargetSetupError; import com.android.tradefed.util.RunUtil; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import org.jf.dexlib2.iface.ClassDef; /* * Target preparer that fetches classpath relevant artifacts for a test in a 'reentrant' manner. * *

The preparer will fetch all BOOTCLASSPATH, SYSTEMSERVERCLASSPATH and shared * libraries from the device, parse their contents and dump the classnames into a temporary * directory.

* *

Additionally, the preparer can fetch, parse and dump data for module specific artifacts * (i.e. apk-in-apex) if specified.

* *

Nested runs of the artifact fetcher (i.e. in the case of a top-level test config xml and a * child test xml) will only fetch non-common elements, and remove temporary class dumps fetched * during that particular preparer run.

* *

Each module's conformance framework test config xml must run the preparer (with module * specific parameters) before the entrypoint test jar, and the top-level conformance framework * config xml must also run it before any other module xml.

* *

The upshot is that when running all conformance framework tests for all modules, the shared * artifacts are fetched and processed only once.

*/ public class ClasspathFetcher extends BaseTargetPreparer { public static final String DEVICE_JAR_ARTIFACTS_TAG = "device-jar-artifacts"; public static final String BCP_CLASSES_FILE = "bcp.pb"; public static final String SSCP_CLASSES_FILE = "sscp.pb"; // TODO(andreionea): also fetch classes for standalone system server jars, apk-in-apex and // shared libraries. They require more mocking on the test side. public static final String APEX_PKG_TAG = "apex-package"; // Special case for fetching only non-updatable platform. public static final String PLATFORM_PACKAGE = "platform"; @Option(name = "apex-package", description = "The package name of the apex under test.") private String mApexPackage; private boolean mFetchedArtifacts = false; @Override public void setUp(TestInformation testInfo) throws TargetSetupError, DeviceNotAvailableException { Objects.requireNonNull(testInfo.getDevice()); if (mApexPackage != null) { testInfo.properties().put(APEX_PKG_TAG, mApexPackage); } // The artifacts have been fetched already, no need to do anything else. if (testInfo.properties().containsKey(DEVICE_JAR_ARTIFACTS_TAG)) { return; } try { final Path tmpDir = Files.createTempDirectory("device_artifacts"); testInfo.properties().put(DEVICE_JAR_ARTIFACTS_TAG, tmpDir.toAbsolutePath().toString()); getClassesInClasspath(testInfo.getDevice(), BOOTCLASSPATH) .writeTo(new FileOutputStream(new File(tmpDir.toFile(), BCP_CLASSES_FILE))); getClassesInClasspath(testInfo.getDevice(), SYSTEMSERVERCLASSPATH) .writeTo(new FileOutputStream(new File(tmpDir.toFile(), SSCP_CLASSES_FILE))); mFetchedArtifacts = true; } catch(IOException e) { throw new RuntimeException("Could not create temp artifacts dir!", e); } } @Override public void tearDown(TestInformation testInfo, Throwable e) { if (mFetchedArtifacts) { try { final String path = testInfo.properties().get(DEVICE_JAR_ARTIFACTS_TAG); if (path == null) { throw new IllegalStateException("Target preparer has previously fetched" + " artifacts, but the DEVICE_JAR_ARTIFACTS_TAG property was removed"); } final File jarArtifactsDir = new File(path); deleteDirectory(jarArtifactsDir); } finally { testInfo.properties().remove(DEVICE_JAR_ARTIFACTS_TAG); } } } private Classpath classpathTypeToClasspathEnum(ClasspathType t) { switch(t) { case BOOTCLASSPATH: return Classpath.valueOf(Classpath.BOOTCLASSPATH_VALUE); case SYSTEMSERVERCLASSPATH: return Classpath.valueOf(Classpath.SYSTEMSERVERCLASSPATH_VALUE); default: throw new RuntimeException("Unknown classpath type " + t); } } private ImmutableSet getClassesInFile(INativeDevice device, String file) throws DeviceNotAvailableException, IOException { final File jar = device.pullFile(file); if (jar == null) { throw new IllegalStateException("could not pull remote file " + file); } return Classpaths.getClassDefsFromJar(jar) .stream() .map(ClassDef::getType) .collect(ImmutableSet.toImmutableSet()); } private ClasspathClassesDump getClassesInClasspath(INativeDevice device, ClasspathType type) throws DeviceNotAvailableException, IOException { ClasspathClassesDump.Builder builder = ClasspathClassesDump.newBuilder(); final ImmutableList jars = Classpaths.getJarsOnClasspath(device, type); for (String jar : jars) { ClasspathEntry.Builder entryBuilder = ClasspathEntry.newBuilder(); Jar.Builder jarBuilder = Jar.newBuilder(); jarBuilder.setClasspath(classpathTypeToClasspathEnum(type)); jarBuilder.setPath(jar); entryBuilder.setJar(jarBuilder.build()); entryBuilder.addAllClasses(getClassesInFile(device, jar)); builder.addEntries(entryBuilder.build()); } return builder.build(); } /** * Deletes a directory and its contents recursively * * @param directory to delete */ private static void deleteDirectory(File directory) { File[] files = directory.listFiles(); if (files != null) { for (File file : files) { if (!file.isDirectory()) { file.delete(); } else { deleteDirectory(file); } } } directory.delete(); } }