blob: 92958311f226b3ce33d619945c683b80a939a2fb [file] [log] [blame]
/*
* Copyright (C) 2023 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.tests.odsign;
import static com.google.common.truth.Truth.assertThat;
import com.android.tradefed.invoker.TestInformation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/** A helper class that can mutate the device state and restore it afterwards. */
public class DeviceState {
private static final String TEST_JAR_RESOURCE_NAME = "/art-gtest-jars-Main.jar";
private static final String PHENOTYPE_FLAG_NAMESPACE = "runtime_native_boot";
private static final String ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME =
OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME + ".bak";
private final TestInformation mTestInfo;
private final OdsignTestUtils mTestUtils;
private Set<String> mTempFiles = new HashSet<>();
private Set<String> mMountPoints = new HashSet<>();
private Map<String, String> mMutatedProperties = new HashMap<>();
private Set<String> mMutatedPhenotypeFlags = new HashSet<>();
private Map<String, String> mDeletedFiles = new HashMap<>();
private boolean mHasArtifactsBackup = false;
public DeviceState(TestInformation testInfo) throws Exception {
mTestInfo = testInfo;
mTestUtils = new OdsignTestUtils(testInfo);
}
/** Restores the device state. */
public void restore() throws Exception {
for (String mountPoint : mMountPoints) {
mTestInfo.getDevice().executeShellV2Command(String.format("umount '%s'", mountPoint));
}
for (String tempFile : mTempFiles) {
mTestInfo.getDevice().deleteFile(tempFile);
}
for (var entry : mMutatedProperties.entrySet()) {
mTestInfo.getDevice().setProperty(
entry.getKey(), entry.getValue() != null ? entry.getValue() : "");
}
for (String flag : mMutatedPhenotypeFlags) {
mTestInfo.getDevice().executeShellV2Command(String.format(
"device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, flag));
}
if (!mMutatedPhenotypeFlags.isEmpty()) {
mTestInfo.getDevice().executeShellV2Command(
"device_config set_sync_disabled_for_tests none");
}
for (var entry : mDeletedFiles.entrySet()) {
mTestInfo.getDevice().executeShellV2Command(
String.format("cp '%s' '%s'", entry.getValue(), entry.getKey()));
mTestInfo.getDevice().executeShellV2Command(String.format("rm '%s'", entry.getValue()));
mTestInfo.getDevice().executeShellV2Command(
String.format("restorecon '%s'", entry.getKey()));
}
if (mHasArtifactsBackup) {
mTestInfo.getDevice().executeShellV2Command(
String.format("rm -rf '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME));
mTestInfo.getDevice().executeShellV2Command(
String.format("mv '%s' '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME,
OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME));
}
}
/** Simulates that the ART APEX has been upgraded. */
public void simulateArtApexUpgrade() throws Exception {
updateApexInfo("com.android.art", false /* isFactory */);
}
/**
* Simulates that the new ART APEX has been uninstalled (i.e., the ART module goes back to the
* factory version).
*/
public void simulateArtApexUninstall() throws Exception {
updateApexInfo("com.android.art", true /* isFactory */);
}
/**
* Simulates that an APEX has been upgraded. We could install a real APEX, but that would
* introduce an extra dependency to this test, which we want to avoid.
*/
public void simulateApexUpgrade() throws Exception {
updateApexInfo("com.android.wifi", false /* isFactory */);
}
/**
* Simulates that the new APEX has been uninstalled (i.e., the module goes back to the factory
* version).
*/
public void simulateApexUninstall() throws Exception {
updateApexInfo("com.android.wifi", true /* isFactory */);
}
private void updateApexInfo(String moduleName, boolean isFactory) throws Exception {
try (var xmlMutator = new XmlMutator(OdsignTestUtils.APEX_INFO_FILE)) {
NodeList list = xmlMutator.getDocument().getElementsByTagName("apex-info");
for (int i = 0; i < list.getLength(); i++) {
Element node = (Element) list.item(i);
if (node.getAttribute("moduleName").equals(moduleName)
&& node.getAttribute("isActive").equals("true")) {
node.setAttribute("isFactory", String.valueOf(isFactory));
node.setAttribute(
"lastUpdateMillis", String.valueOf(System.currentTimeMillis()));
}
}
}
}
/** Simulates that there is an OTA that updates a boot classpath jar. */
public void simulateBootClasspathOta() throws Exception {
File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME);
pushAndBindMount(localFile, "/system/framework/framework.jar");
}
/** Simulates that there is an OTA that updates a system server jar. */
public void simulateSystemServerOta() throws Exception {
File localFile = mTestUtils.copyResourceToFile(TEST_JAR_RESOURCE_NAME);
pushAndBindMount(localFile, "/system/framework/services.jar");
}
public void makeDex2oatFail() throws Exception {
setProperty("dalvik.vm.boot-dex2oat-threads", "-1");
}
/** Sets a system property. */
public void setProperty(String key, String value) throws Exception {
if (!mMutatedProperties.containsKey(key)) {
// Backup the original value.
mMutatedProperties.put(key, mTestInfo.getDevice().getProperty(key));
}
mTestInfo.getDevice().setProperty(key, value);
}
/** Sets a phenotype flag. */
public void setPhenotypeFlag(String key, String value) throws Exception {
if (!mMutatedPhenotypeFlags.contains(key)) {
// Tests assume that phenotype flags are initially not set. Check if the assumption is
// true.
assertThat(mTestUtils.assertCommandSucceeds(String.format(
"device_config get '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key)))
.isEqualTo("null");
mMutatedPhenotypeFlags.add(key);
}
// Disable phenotype flag syncing. Potentially, we can set `set_sync_disabled_for_tests` to
// `until_reboot`, but setting it to `persistent` prevents unrelated system crashes/restarts
// from affecting the test. `set_sync_disabled_for_tests` is reset in `restore` anyway.
mTestUtils.assertCommandSucceeds("device_config set_sync_disabled_for_tests persistent");
if (value != null) {
mTestUtils.assertCommandSucceeds(String.format(
"device_config put '%s' '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key, value));
} else {
mTestUtils.assertCommandSucceeds(
String.format("device_config delete '%s' '%s'", PHENOTYPE_FLAG_NAMESPACE, key));
}
}
public void backupAndDeleteFile(String remotePath) throws Exception {
String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp";
// Backup the file before deleting it.
mTestUtils.assertCommandSucceeds(String.format("cp '%s' '%s'", remotePath, tempFile));
mTestUtils.assertCommandSucceeds(String.format("rm '%s'", remotePath));
mDeletedFiles.put(remotePath, tempFile);
}
public void backupArtifacts() throws Exception {
mTestInfo.getDevice().executeShellV2Command(
String.format("rm -rf '%s'", ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME));
mTestUtils.assertCommandSucceeds(
String.format("cp -r '%s' '%s'", OdsignTestUtils.ART_APEX_DALVIK_CACHE_DIRNAME,
ART_APEX_DALVIK_CACHE_BACKUP_DIRNAME));
mHasArtifactsBackup = true;
}
/**
* Pushes the file to a temporary location and bind-mount it at the given path. This is useful
* when the path is readonly.
*/
private void pushAndBindMount(File localFile, String remotePath) throws Exception {
String tempFile = "/data/local/tmp/odsign_e2e_tests_" + UUID.randomUUID() + ".tmp";
assertThat(mTestInfo.getDevice().pushFile(localFile, tempFile)).isTrue();
mTempFiles.add(tempFile);
// If the path has already been bind-mounted by this method before, unmount it first.
if (mMountPoints.contains(remotePath)) {
mTestUtils.assertCommandSucceeds(String.format("umount '%s'", remotePath));
mMountPoints.remove(remotePath);
}
mTestUtils.assertCommandSucceeds(
String.format("mount --bind '%s' '%s'", tempFile, remotePath));
mMountPoints.add(remotePath);
mTestUtils.assertCommandSucceeds(String.format("restorecon '%s'", remotePath));
}
/** A helper class for mutating an XML file. */
private class XmlMutator implements AutoCloseable {
private final Document mDocument;
private final String mRemoteXmlFile;
private final File mLocalFile;
public XmlMutator(String remoteXmlFile) throws Exception {
// Load the XML file.
mRemoteXmlFile = remoteXmlFile;
mLocalFile = mTestInfo.getDevice().pullFile(remoteXmlFile);
assertThat(mLocalFile).isNotNull();
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
mDocument = builder.parse(mLocalFile);
}
@Override
public void close() throws Exception {
// Save the XML file.
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.transform(new DOMSource(mDocument), new StreamResult(mLocalFile));
pushAndBindMount(mLocalFile, mRemoteXmlFile);
}
/** Returns a mutable XML document. */
public Document getDocument() {
return mDocument;
}
}
}