Merge changes from topic "sdkext-base-cf-test"

* changes:
  Create base conformance framework test
  Add fetch code for classpath jars
This commit is contained in:
Andrei-Valentin Onea
2022-12-20 16:49:42 +00:00
committed by Gerrit Code Review
10 changed files with 484 additions and 5 deletions

View File

@@ -24,4 +24,12 @@ java_library_host {
"compatibility-host-util",
"androidx.annotation_annotation",
],
static_libs: [
"compat-classpaths-testing",
"classpath_classes_proto_java"
],
visibility: [
"//packages/modules/common:__subpackages__",
"//packages/modules/SdkExtensions",
],
}

View File

@@ -16,19 +16,37 @@
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.
@@ -54,20 +72,43 @@ import java.nio.file.Path;
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());
// TODO(b/254647172): Fetch data
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);
@@ -84,13 +125,70 @@ public class ClasspathFetcher extends BaseTargetPreparer {
+ " artifacts, but the DEVICE_JAR_ARTIFACTS_TAG property was removed");
}
final File jarArtifactsDir = new File(path);
if (!jarArtifactsDir.delete()) {
throw new RuntimeException("Failed to remove jar artifacts dir!");
}
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<String> 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<String> 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();
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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 {
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_test_host {
name: "ConformanceFrameworkTests",
srcs: ["*.java"],
static_libs: [
"junit",
"ClasspathFetcher",
"truth-prebuilt",
"objenesis",
],
libs: [
"cts-tradefed",
"tradefed",
"compatibility-host-util",
],
test_suites: [
"general-tests",
],
}

View File

@@ -0,0 +1,186 @@
/*
* 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.conformanceframework;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assume.assumeTrue;
import com.android.modules.proto.ClasspathClasses.ClasspathClassesDump;
import com.android.modules.proto.ClasspathClasses.ClasspathEntry;
import com.android.modules.proto.ClasspathClasses.Jar;
import com.android.modules.targetprep.ClasspathFetcher;
import com.android.modules.utils.build.testing.DeviceSdkLevel;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
import com.android.tradefed.testtype.junit4.BeforeClassWithInfo;
import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.jf.dexlib2.iface.ClassDef;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Tests for detecting no duplicate class files are present on BOOTCLASSPATH and
* SYSTEMSERVERCLASSPATH.
*
* <p>Duplicate class files are not safe as some of the jars on *CLASSPATH are updated outside of
* the main dessert release cycle; they also contribute to unnecessary disk space usage.
*/
@RunWith(DeviceJUnit4ClassRunner.class)
public class DuplicateClassesTest extends BaseHostJUnit4Test {
private static ImmutableSet<String> sBootclasspathJars;
private static ImmutableSet<String> sSystemserverclasspathJars;
private static ImmutableMultimap<String, String> sJarsToClasses;
private static String sApexPackage;
private DeviceSdkLevel mDeviceSdkLevel;
/**
* Fetch all classpath info extracted by ClasspathFetcher.
*
*/
@BeforeClassWithInfo
public static void setupOnce(TestInformation testInfo) throws Exception {
final String dctArtifactsPath = Objects.requireNonNull(
testInfo.properties().get(ClasspathFetcher.DEVICE_JAR_ARTIFACTS_TAG));
sApexPackage = testInfo.properties().get(ClasspathFetcher.APEX_PKG_TAG);
final ImmutableMultimap.Builder<String, String> jarsToClasses =
new ImmutableMultimap.Builder<>();
final File bcpDumpFile = new File(dctArtifactsPath, ClasspathFetcher.BCP_CLASSES_FILE);
final ClasspathClassesDump bcpDump =
ClasspathClassesDump.parseFrom(new FileInputStream(bcpDumpFile));
sBootclasspathJars = bcpDump.getEntriesList().stream()
.map(entry -> entry.getJar().getPath())
.collect(ImmutableSet.toImmutableSet());
bcpDump.getEntriesList().stream()
.forEach(entry -> {
jarsToClasses.putAll(entry.getJar().getPath(), entry.getClassesList());
});
final File sscpDumpFile = new File(dctArtifactsPath, ClasspathFetcher.SSCP_CLASSES_FILE);
final ClasspathClassesDump sscpDump =
ClasspathClassesDump.parseFrom(new FileInputStream(sscpDumpFile));
sSystemserverclasspathJars = sscpDump.getEntriesList().stream()
.map(entry -> entry.getJar().getPath())
.collect(ImmutableSet.toImmutableSet());
sscpDump.getEntriesList().stream()
.forEach(entry -> {
jarsToClasses.putAll(entry.getJar().getPath(), entry.getClassesList());
});
sJarsToClasses = jarsToClasses.build();
}
@Before
public void setup() {
mDeviceSdkLevel = new DeviceSdkLevel(getDevice());
}
/**
* Ensure that there are no duplicate classes among jars listed in BOOTCLASSPATH.
*/
@Test
public void testBootclasspath_nonDuplicateClasses() throws Exception {
assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
assertThat(getDuplicateClasses(sBootclasspathJars)).isEmpty();
}
/**
* Ensure that there are no duplicate classes among jars listed in SYSTEMSERVERCLASSPATH.
*/
@Test
public void testSystemserverClasspath_nonDuplicateClasses() throws Exception {
assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
assertThat(getDuplicateClasses(sSystemserverclasspathJars)).isEmpty();
}
/**
* Ensure that there are no duplicate classes among jars listed in BOOTCLASSPATH and
* SYSTEMSERVERCLASSPATH.
*/
@Test
public void testSystemserverAndBootClasspath_nonDuplicateClasses() throws Exception {
assumeTrue(mDeviceSdkLevel.isDeviceAtLeastR());
final ImmutableSet.Builder<String> jars = new ImmutableSet.Builder<>();
jars.addAll(sBootclasspathJars);
jars.addAll(sSystemserverclasspathJars);
assertThat(getDuplicateClasses(jars.build())).isEmpty();
}
/**
* Gets the duplicate classes within a list of jar files.
*
* @param jars a list of jar files.
* @return a multimap with the class name as a key and the jar files as a value.
*/
private Multimap<String, String> getDuplicateClasses(ImmutableCollection<String> jars) {
final HashMultimap<String, String> allClasses = HashMultimap.create();
Multimaps.invertFrom(Multimaps.filterKeys(sJarsToClasses, jars::contains), allClasses);
return Multimaps.filterKeys(allClasses, key -> validDuplicates(allClasses.get(key)));
}
/**
* Filtering function for excluding invalid / uninteresting duplicates.
*
* This will filter out classes that are in only 1 jar, or duplicates that
* do not include jars in the apex under test.
*/
private boolean validDuplicates(Collection<String> duplicateJars) {
if (duplicateJars.size() <= 1) {
return false;
}
if (sApexPackage.equals(ClasspathFetcher.PLATFORM_PACKAGE)) {
return duplicateJars.stream()
.anyMatch(jar -> !jar.startsWith("/apex"));
}
final String apexPrefix = "/apex/" + sApexPackage;
return duplicateJars.stream()
.anyMatch(jar -> jar.startsWith(apexPrefix));
}
}

View File

@@ -34,6 +34,10 @@ java_test_host {
"junit",
"tradefed",
],
java_resources: [
":LibraryA",
":LibraryB",
],
test_suites: [
"general-tests",
],

View File

@@ -22,17 +22,30 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.android.modules.proto.ClasspathClasses.ClasspathClassesDump;
import com.android.modules.proto.ClasspathClasses.ClasspathEntry;
import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.invoker.IInvocationContext;
import com.android.tradefed.invoker.InvocationContext;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.List;
import org.junit.Assert;
import org.junit.Before;
@@ -46,6 +59,7 @@ import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;
@RunWith(JUnit4.class)
public class ClasspathFetcherTest {
@@ -57,6 +71,9 @@ public class ClasspathFetcherTest {
private TestInformation mTestInfo;
private String mBootclasspathJarNames = "";
private String mSystemServerclasspathJarNames = "";
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -64,6 +81,28 @@ public class ClasspathFetcherTest {
when(mMockTestDevice.getSerialNumber()).thenReturn(SERIAL);
when(mMockTestDevice.getDeviceDescriptor()).thenReturn(null);
when(mMockTestDevice.isAppEnumerationSupported()).thenReturn(false);
when(mMockTestDevice.executeShellV2Command(eq("echo $BOOTCLASSPATH"))).then(
invocation -> {
return successfulCommandResult(mBootclasspathJarNames, "");
}
);
when(mMockTestDevice.executeShellV2Command(eq("echo $SYSTEMSERVERCLASSPATH"))).then(
invocation -> {
return successfulCommandResult(mSystemServerclasspathJarNames, "");
}
);
when(mMockTestDevice.pullFile(anyString())).then(
invocation -> {
final String path = invocation.getArgument(0);
final File tempFile = File.createTempFile(path, null);
try (InputStream is =
ClasspathFetcherTest.class.getClassLoader().getResourceAsStream(path)) {
Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
return tempFile;
}
);
IInvocationContext context = new InvocationContext();
context.addAllocatedDevice("device", mMockTestDevice);
context.addDeviceBuildInfo("device", mMockBuildInfo);
@@ -72,6 +111,8 @@ public class ClasspathFetcherTest {
@Test
public void testSingleArtifactFetcher() throws Exception {
mBootclasspathJarNames = "LibraryA.jar";
mSystemServerclasspathJarNames = "LibraryB.jar";
final ClasspathFetcher fetcher = new ClasspathFetcher();
fetcher.setUp(mTestInfo);
assertThat(mTestInfo.properties().containsKey(DEVICE_JAR_ARTIFACTS_TAG)).isTrue();
@@ -81,6 +122,8 @@ public class ClasspathFetcherTest {
@Test
public void testMultipleArtifactFetchers() throws Exception {
mBootclasspathJarNames = "LibraryA.jar";
mSystemServerclasspathJarNames = "LibraryB.jar";
final ClasspathFetcher fetcher1 = new ClasspathFetcher();
final ClasspathFetcher fetcher2 = new ClasspathFetcher();
@@ -92,4 +135,68 @@ public class ClasspathFetcherTest {
fetcher1.tearDown(mTestInfo, null);
assertThat(mTestInfo.properties().containsKey(DEVICE_JAR_ARTIFACTS_TAG)).isFalse();
}
@Test
public void testFetchCorrectBcpClasses() throws Exception {
mBootclasspathJarNames = "LibraryA.jar";
mSystemServerclasspathJarNames = "LibraryB.jar";
final ClasspathFetcher fetcher = new ClasspathFetcher();
try {
fetcher.setUp(mTestInfo);
final File bcpProto = new File(mTestInfo.properties().get(DEVICE_JAR_ARTIFACTS_TAG),
ClasspathFetcher.BCP_CLASSES_FILE);
assertThat(bcpProto.exists()).isTrue();
ClasspathClassesDump dump =
ClasspathClassesDump.parseFrom(new FileInputStream(bcpProto));
List<ClasspathEntry> entries = dump.getEntriesList();
assertThat(entries.size()).isEqualTo(1);
ClasspathEntry entry = entries.get(0);
assertThat(entry.hasJar()).isTrue();
assertThat(entry.getJar().getPath()).isEqualTo("LibraryA.jar");
assertThat(entry.getClassesList().size()).isEqualTo(1);
assertThat(entry.getClassesList().get(0))
.isEqualTo("Lcom/android/modules/targetprep/android/A;");
} finally {
fetcher.tearDown(mTestInfo, null);
}
}
@Test
public void testFetchCorrectSscpClasses() throws Exception {
mBootclasspathJarNames = "LibraryA.jar";
mSystemServerclasspathJarNames = "LibraryB.jar";
final ClasspathFetcher fetcher = new ClasspathFetcher();
try {
fetcher.setUp(mTestInfo);
final File sscpProto = new File(mTestInfo.properties().get(DEVICE_JAR_ARTIFACTS_TAG),
ClasspathFetcher.SSCP_CLASSES_FILE);
assertThat(sscpProto.exists()).isTrue();
ClasspathClassesDump dump =
ClasspathClassesDump.parseFrom(new FileInputStream(sscpProto));
List<ClasspathEntry> entries = dump.getEntriesList();
assertThat(entries.size()).isEqualTo(1);
ClasspathEntry entry = entries.get(0);
assertThat(entry.hasJar()).isTrue();
assertThat(entry.getJar().getPath()).isEqualTo("LibraryB.jar");
assertThat(entry.getClassesList().size()).isEqualTo(1);
assertThat(entry.getClassesList().get(0))
.isEqualTo("Lcom/android/modules/targetprep/android/B;");
} finally {
fetcher.tearDown(mTestInfo, null);
}
}
private static CommandResult successfulCommandResult(String stdout, String stderr) {
final CommandResult result = new CommandResult();
result.setStatus(CommandStatus.SUCCESS);
result.setExitCode(0);
result.setStdout(stdout);
result.setStderr(stderr);
return result;
}
}

View File

@@ -0,0 +1,3 @@
package com.android.modules.targetprep.android;
public class A {}

View File

@@ -0,0 +1,31 @@
/*
* 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 {
default_applicable_licenses: ["Android-Apache-2.0"],
}
java_library {
name: "LibraryA",
srcs: ["A.java"],
installable: true,
}
java_library {
name: "LibraryB",
srcs: ["B.java"],
installable: true,
}

View File

@@ -0,0 +1,3 @@
package com.android.modules.targetprep.android;
public class B {}

View File

@@ -14,6 +14,7 @@
package {
default_applicable_licenses: ["Android-Apache-2.0"],
default_visibility: ["//packages/modules/common/javatests:__subpackages__"],
}
android_test_helper_app {