Merge "Add RunHostScriptTargetPreparer"
diff --git a/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
new file mode 100644
index 0000000..346c86e
--- /dev/null
+++ b/test_framework/com/android/tradefed/targetprep/RunHostScriptTargetPreparer.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2020 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.tradefed.targetprep;
+
+import com.android.tradefed.config.GlobalConfiguration;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.IRunUtil;
+import com.android.tradefed.util.RunUtil;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+/**
+ * Target preparer which executes a script before running a test. The script can reference the
+ * device's serial number using the SERIAL environment variable.
+ */
+@OptionClass(alias = "run-host-script")
+public class RunHostScriptTargetPreparer extends BaseTargetPreparer {
+
+ @Option(name = "script-file", description = "Path to the script to execute.")
+ private String mScriptPath = null;
+
+ @Option(name = "work-dir", description = "Working directory to use when executing script.")
+ private File mWorkDir = null;
+
+ @Option(name = "script-timeout", description = "Script execution timeout.")
+ private Duration mTimeout = Duration.ofMinutes(1L);
+
+ private IRunUtil mRunUtil;
+
+ @Override
+ public void setUp(TestInformation testInfo)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+ if (mScriptPath == null) {
+ CLog.w("No script to execute.");
+ return;
+ }
+ ITestDevice device = testInfo.getDevice();
+
+ // Find script and verify it exists and is executable
+ File scriptFile = findScriptFile(testInfo);
+ if (scriptFile == null || !scriptFile.isFile()) {
+ throw new TargetSetupError(
+ String.format("File %s not found.", mScriptPath), device.getDeviceDescriptor());
+ }
+ if (!scriptFile.canExecute()) {
+ scriptFile.setExecutable(true);
+ }
+
+ // Set working directory and environment variables
+ getRunUtil().setWorkingDir(mWorkDir);
+ getRunUtil().setEnvVariable("SERIAL", device.getSerialNumber());
+ setPathVariable(testInfo);
+
+ // Execute script and handle result
+ CommandResult result =
+ getRunUtil().runTimedCmd(mTimeout.toMillis(), scriptFile.getAbsolutePath());
+ switch (result.getStatus()) {
+ case SUCCESS:
+ CLog.i("Script executed successfully, stdout = [%s].", result.getStdout());
+ break;
+ case FAILED:
+ throw new TargetSetupError(
+ String.format(
+ "Script execution failed, stdout = [%s], stderr = [%s].",
+ result.getStdout(), result.getStderr()),
+ device.getDeviceDescriptor());
+ case TIMED_OUT:
+ throw new TargetSetupError(
+ "Script execution timed out.", device.getDeviceDescriptor());
+ case EXCEPTION:
+ throw new TargetSetupError(
+ String.format(
+ "Exception during script execution, stdout = [%s], stderr = [%s].",
+ result.getStdout(), result.getStderr()),
+ device.getDeviceDescriptor());
+ }
+ }
+
+ /** @return {@link IRunUtil} instance to use */
+ @VisibleForTesting
+ IRunUtil getRunUtil() {
+ if (mRunUtil == null) {
+ mRunUtil = new RunUtil();
+ }
+ return mRunUtil;
+ }
+
+ /** @return {@link IDeviceManager} instance used to fetch the configured adb/fastboot paths */
+ @VisibleForTesting
+ IDeviceManager getDeviceManager() {
+ return GlobalConfiguration.getDeviceManagerInstance();
+ }
+
+ /** Find the script file to execute. */
+ @Nullable
+ private File findScriptFile(TestInformation testInfo) {
+ File scriptFile = new File(mScriptPath);
+ if (scriptFile.isAbsolute()) {
+ return scriptFile;
+ }
+ // Try to find the script in the working directory if it was set
+ if (mWorkDir != null) {
+ scriptFile = new File(mWorkDir, mScriptPath);
+ if (scriptFile.isFile()) {
+ return scriptFile;
+ }
+ }
+ // Otherwise, search for it in the test information
+ try {
+ return testInfo.getDependencyFile(mScriptPath, false);
+ } catch (FileNotFoundException e) {
+ return null;
+ }
+ }
+
+ /** Update $PATH if necessary to use consistent adb and fastboot binaries. */
+ private void setPathVariable(TestInformation testInfo) {
+ List<String> paths = new ArrayList<>();
+ // Use the test's adb binary or the globally defined one
+ File adbBinary = testInfo.executionFiles().get(FilesKey.ADB_BINARY);
+ if (adbBinary == null) {
+ String adbPath = getDeviceManager().getAdbPath();
+ if (!adbPath.equals("adb")) { // ignore default binary
+ adbBinary = new File(adbPath);
+ }
+ }
+ if (adbBinary != null && adbBinary.isFile()) {
+ paths.add(adbBinary.getParentFile().getAbsolutePath());
+ }
+ // Use the globally defined fastboot binary
+ String fastbootPath = getDeviceManager().getFastbootPath();
+ if (!fastbootPath.equals("fastboot")) { // ignore default binary
+ File fastbootBinary = new File(fastbootPath);
+ if (fastbootBinary.isFile()) {
+ paths.add(fastbootBinary.getParentFile().getAbsolutePath());
+ }
+ }
+ // Prepend additional paths to the PATH variable
+ if (!paths.isEmpty()) {
+ String separator = System.getProperty("path.separator");
+ paths.add(System.getenv("PATH"));
+ String path = paths.stream().distinct().collect(Collectors.joining(separator));
+ CLog.d("Using updated $PATH: %s", path);
+ getRunUtil().setEnvVariable("PATH", path);
+ }
+ }
+}
diff --git a/tests/src/com/android/tradefed/UnitTests.java b/tests/src/com/android/tradefed/UnitTests.java
index 45594a4..101caa8 100644
--- a/tests/src/com/android/tradefed/UnitTests.java
+++ b/tests/src/com/android/tradefed/UnitTests.java
@@ -235,6 +235,7 @@
import com.android.tradefed.targetprep.RootTargetPreparerTest;
import com.android.tradefed.targetprep.RunCommandTargetPreparerTest;
import com.android.tradefed.targetprep.RunHostCommandTargetPreparerTest;
+import com.android.tradefed.targetprep.RunHostScriptTargetPreparerTest;
import com.android.tradefed.targetprep.StopServicesSetupTest;
import com.android.tradefed.targetprep.SwitchUserTargetPreparerTest;
import com.android.tradefed.targetprep.SystemUpdaterDeviceFlasherTest;
@@ -674,6 +675,7 @@
RootTargetPreparerTest.class,
RunCommandTargetPreparerTest.class,
RunHostCommandTargetPreparerTest.class,
+ RunHostScriptTargetPreparerTest.class,
StopServicesSetupTest.class,
SystemUpdaterDeviceFlasherTest.class,
TargetSetupErrorTest.class,
diff --git a/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
new file mode 100644
index 0000000..ee43e22
--- /dev/null
+++ b/tests/src/com/android/tradefed/targetprep/RunHostScriptTargetPreparerTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 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.tradefed.targetprep;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.android.tradefed.config.ConfigurationException;
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.device.IDeviceManager;
+import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.FileUtil;
+import com.android.tradefed.util.IRunUtil;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/** Unit test for {@link RunHostScriptTargetPreparer}. */
+@RunWith(JUnit4.class)
+public final class RunHostScriptTargetPreparerTest {
+
+ private static final String DEVICE_SERIAL = "DEVICE_SERIAL";
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private TestInformation mTestInfo;
+
+ @Mock private IRunUtil mRunUtil;
+ @Mock private IDeviceManager mDeviceManager;
+ private RunHostScriptTargetPreparer mPreparer;
+ private OptionSetter mOptionSetter;
+ private File mWorkDir;
+ private File mScriptFile;
+
+ @Before
+ public void setUp() throws IOException, ConfigurationException {
+ // Initialize preparer and test information
+ mPreparer =
+ new RunHostScriptTargetPreparer() {
+ @Override
+ IRunUtil getRunUtil() {
+ return mRunUtil;
+ }
+
+ @Override
+ IDeviceManager getDeviceManager() {
+ return mDeviceManager;
+ }
+ };
+ mOptionSetter = new OptionSetter(mPreparer);
+ when(mTestInfo.getDevice().getSerialNumber()).thenReturn(DEVICE_SERIAL);
+ when(mTestInfo.executionFiles().get(any(FilesKey.class))).thenReturn(null);
+
+ // Create temporary working directory and script file
+ mWorkDir = FileUtil.createTempDir(this.getClass().getSimpleName());
+ mScriptFile = File.createTempFile("script", ".sh", mWorkDir);
+
+ // Default to default adb/fastboot paths
+ when(mDeviceManager.getAdbPath()).thenReturn("adb");
+ when(mDeviceManager.getFastbootPath()).thenReturn("fastboot");
+
+ // Default to successful execution
+ CommandResult result = new CommandResult(CommandStatus.SUCCESS);
+ when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+ }
+
+ @After
+ public void tearDown() {
+ FileUtil.recursiveDelete(mWorkDir);
+ }
+
+ @Test
+ public void testSetUp() throws Exception {
+ mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+ mOptionSetter.setOptionValue("script-timeout", "10");
+ // Verify environment, timeout, and script path
+ mPreparer.setUp(mTestInfo);
+ verify(mRunUtil).setEnvVariable("SERIAL", DEVICE_SERIAL);
+ verify(mRunUtil, never()).setEnvVariable(eq("PATH"), any()); // uses default PATH
+ verify(mRunUtil).runTimedCmd(10L, mScriptFile.getAbsolutePath());
+ // Verify that script is executable
+ assertTrue(mScriptFile.canExecute());
+ }
+
+ @Test
+ public void testSetUp_workingDir() throws Exception {
+ mOptionSetter.setOptionValue("work-dir", mWorkDir.getAbsolutePath());
+ mOptionSetter.setOptionValue("script-file", mScriptFile.getName()); // relative
+ // Verify that the working directory is set and script's path is resolved
+ mPreparer.setUp(mTestInfo);
+ verify(mRunUtil).setWorkingDir(mWorkDir);
+ verify(mRunUtil).runTimedCmd(anyLong(), eq(mScriptFile.getAbsolutePath()));
+ }
+
+ @Test
+ public void testSetUp_findFile() throws Exception {
+ mOptionSetter.setOptionValue("script-file", mScriptFile.getName()); // relative
+ when(mTestInfo.getDependencyFile(any(), anyBoolean())).thenReturn(mScriptFile);
+ // Verify that the script is found in the test information
+ mPreparer.setUp(mTestInfo);
+ verify(mRunUtil).runTimedCmd(anyLong(), eq(mScriptFile.getAbsolutePath()));
+ }
+
+ @Test(expected = TargetSetupError.class)
+ public void testSetUp_fileNotFound() throws Exception {
+ mOptionSetter.setOptionValue("script-file", "unknown.sh");
+ mPreparer.setUp(mTestInfo);
+ }
+
+ @Test(expected = TargetSetupError.class)
+ public void testSetUp_executionError() throws Exception {
+ mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+ CommandResult result = new CommandResult(CommandStatus.FAILED);
+ when(mRunUtil.runTimedCmd(anyLong(), any())).thenReturn(result);
+ mPreparer.setUp(mTestInfo);
+ }
+
+ @Test
+ public void testSetUp_pathVariable() throws Exception {
+ mOptionSetter.setOptionValue("script-file", mScriptFile.getAbsolutePath());
+ // Create and set dummy adb binary
+ Path adbDir = Files.createTempDirectory(mWorkDir.toPath(), "adb");
+ File adbBinary = File.createTempFile("adb", ".sh", adbDir.toFile());
+ when(mTestInfo.executionFiles().get(eq(FilesKey.ADB_BINARY))).thenReturn(adbBinary);
+ // Create and set dummy fastboot binary
+ Path fastbootDir = Files.createTempDirectory(mWorkDir.toPath(), "fastboot");
+ File fastbootBinary = File.createTempFile("fastboot", ".sh", fastbootDir.toFile());
+ when(mDeviceManager.getFastbootPath()).thenReturn(fastbootBinary.getAbsolutePath());
+ // Verify that binary paths were prepended to the path variable
+ String separator = System.getProperty("path.separator");
+ String expectedPath = adbDir + separator + fastbootDir + separator + System.getenv("PATH");
+ mPreparer.setUp(mTestInfo);
+ verify(mRunUtil).setEnvVariable("PATH", expectedPath);
+ }
+}