| /* |
| * Copyright (C) 2018 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 android.signature.cts.api; |
| |
| import android.app.Instrumentation; |
| import android.os.Bundle; |
| import android.provider.Settings; |
| import android.signature.cts.ApiDocumentParser; |
| import android.signature.cts.ClassProvider; |
| import android.signature.cts.ExcludingClassProvider; |
| import android.signature.cts.ExpectedFailuresFilter; |
| import android.signature.cts.FailureType; |
| import android.signature.cts.JDiffClassDescription; |
| import android.signature.cts.ResultObserver; |
| import android.signature.cts.VirtualPath; |
| import android.signature.cts.VirtualPath.LocalFilePath; |
| import android.signature.cts.VirtualPath.ResourcePath; |
| import android.util.Log; |
| |
| import androidx.test.platform.app.InstrumentationRegistry; |
| |
| import com.android.compatibility.common.util.DynamicConfigDeviceSide; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.StandardOpenOption; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.EnumSet; |
| import java.util.List; |
| import java.util.stream.Stream; |
| import java.util.zip.ZipFile; |
| import junit.framework.TestCase; |
| |
| /** |
| */ |
| public class AbstractApiTest extends TestCase { |
| |
| /** |
| * The name of the optional instrumentation option that contains the name of the dynamic config |
| * data set that contains the expected failures. |
| */ |
| private static final String DYNAMIC_CONFIG_NAME_OPTION = "dynamic-config-name"; |
| |
| private static final String TAG = "SignatureTest"; |
| |
| private TestResultObserver mResultObserver; |
| |
| ClassProvider mClassProvider; |
| |
| /** |
| * The list of expected failures. |
| */ |
| private Collection<String> expectedFailures = Collections.emptyList(); |
| |
| public Instrumentation getInstrumentation() { |
| return InstrumentationRegistry.getInstrumentation(); |
| } |
| |
| protected String getGlobalExemptions() { |
| return Settings.Global.getString( |
| getInstrumentation().getContext().getContentResolver(), |
| Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS); |
| } |
| |
| protected String getGlobalHiddenApiPolicy() { |
| return Settings.Global.getString( |
| getInstrumentation().getContext().getContentResolver(), |
| Settings.Global.HIDDEN_API_POLICY); |
| } |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| mResultObserver = new TestResultObserver(); |
| |
| // Get the arguments passed to the instrumentation. |
| Bundle instrumentationArgs = InstrumentationRegistry.getArguments(); |
| |
| // Check that the device is in the correct state for running this test. |
| assertEquals( |
| String.format("Device in bad state: %s is not as expected", |
| Settings.Global.HIDDEN_API_BLACKLIST_EXEMPTIONS), |
| getExpectedBlocklistExemptions(), |
| getGlobalExemptions()); |
| assertEquals( |
| String.format("Device in bad state: %s is not as expected", |
| Settings.Global.HIDDEN_API_POLICY), |
| null, |
| getGlobalHiddenApiPolicy()); |
| |
| |
| // Prepare for a class provider that loads classes from bootclasspath but filters |
| // out known inaccessible classes. |
| // Note that com.android.internal.R.* inner classes are also excluded as they are |
| // not part of API though exist in the runtime. |
| mClassProvider = new ExcludingClassProvider( |
| new BootClassPathClassesProvider(), |
| name -> name != null && name.startsWith("com.android.internal.R.")); |
| |
| String dynamicConfigName = instrumentationArgs.getString(DYNAMIC_CONFIG_NAME_OPTION); |
| if (dynamicConfigName != null) { |
| // Get the DynamicConfig.xml contents and extract the expected failures list. |
| DynamicConfigDeviceSide dcds = new DynamicConfigDeviceSide(dynamicConfigName); |
| Collection<String> expectedFailures = dcds.getValues("expected_failures"); |
| initExpectedFailures(expectedFailures); |
| } |
| |
| initializeFromArgs(instrumentationArgs); |
| } |
| |
| /** |
| * Initialize the expected failures. |
| * |
| * <p>Call from with {@link #setUp()}</p> |
| * |
| * @param expectedFailures the expected failures. |
| */ |
| private void initExpectedFailures(Collection<String> expectedFailures) { |
| this.expectedFailures = expectedFailures; |
| String tag = getClass().getName(); |
| Log.d(tag, "Expected failure count: " + expectedFailures.size()); |
| for (String failure: expectedFailures) { |
| Log.d(tag, "Expected failure: \"" + failure + "\""); |
| } |
| } |
| |
| protected String getExpectedBlocklistExemptions() { |
| return null; |
| } |
| |
| protected void initializeFromArgs(Bundle instrumentationArgs) throws Exception { |
| } |
| |
| protected interface RunnableWithResultObserver { |
| void run(ResultObserver observer) throws Exception; |
| } |
| |
| void runWithTestResultObserver(RunnableWithResultObserver runnable) { |
| runWithTestResultObserver(expectedFailures, runnable); |
| } |
| |
| private void runWithTestResultObserver( |
| Collection<String> expectedFailures, RunnableWithResultObserver runnable) { |
| try { |
| ResultObserver observer = mResultObserver; |
| if (!expectedFailures.isEmpty()) { |
| observer = new ExpectedFailuresFilter(observer, expectedFailures); |
| } |
| runnable.run(observer); |
| } catch (Error|Exception e) { |
| mResultObserver.notifyFailure( |
| FailureType.CAUGHT_EXCEPTION, |
| e.getClass().getName(), |
| "Uncaught exception thrown by test", |
| e); |
| } |
| mResultObserver.onTestComplete(); // Will throw is there are failures |
| } |
| |
| static String[] getCommaSeparatedListOptional(Bundle instrumentationArgs, String key) { |
| String argument = instrumentationArgs.getString(key); |
| if (argument == null) { |
| return new String[0]; |
| } |
| return argument.split(","); |
| } |
| |
| static String[] getCommaSeparatedListRequired(Bundle instrumentationArgs, String key) { |
| String argument = instrumentationArgs.getString(key); |
| if (argument == null) { |
| throw new IllegalStateException("Could not find required argument '" + key + "'"); |
| } |
| return argument.split(","); |
| } |
| |
| private Stream<VirtualPath> readResource(String resourceName) { |
| try { |
| ResourcePath resourcePath = |
| VirtualPath.get(getClass().getClassLoader(), resourceName); |
| if (resourceName.endsWith(".zip")) { |
| // Extract to a temporary file and read from there. |
| Path file = extractResourceToFile(resourceName, resourcePath.newInputStream()); |
| return flattenPaths(VirtualPath.get(file.toString())); |
| } else { |
| return Stream.of(resourcePath); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| Path extractResourceToFile(String resourceName, InputStream is) throws IOException { |
| Path tempDirectory = Files.createTempDirectory("signature"); |
| Path file = tempDirectory.resolve(resourceName); |
| Log.i(TAG, "extractResourceToFile: extracting " + resourceName + " to " + file); |
| Files.copy(is, file); |
| is.close(); |
| return file; |
| } |
| |
| /** |
| * Given a path in the local file system (possibly of a zip file) flatten it into a stream of |
| * virtual paths. |
| */ |
| private Stream<VirtualPath> flattenPaths(LocalFilePath path) { |
| try { |
| if (path.toString().endsWith(".zip")) { |
| return getZipEntryFiles(path); |
| } else { |
| return Stream.of(path); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| Stream<JDiffClassDescription> parseApiResourcesAsStream( |
| ApiDocumentParser apiDocumentParser, String[] apiResources) { |
| return Stream.of(apiResources) |
| .flatMap(this::readResource) |
| .flatMap(apiDocumentParser::parseAsStream); |
| } |
| |
| /** |
| * Get the zip entries that are files. |
| * |
| * @param path the path to the zip file. |
| * @return paths to zip entries |
| */ |
| protected Stream<VirtualPath> getZipEntryFiles(LocalFilePath path) throws IOException { |
| @SuppressWarnings("resource") |
| ZipFile zip = new ZipFile(path.toFile()); |
| return zip.stream().map(entry -> VirtualPath.get(zip, entry)); |
| } |
| } |