Merge "Make UpdatableSystemFontTest device side test." into sc-dev am: 129c595177 am: 634d72c9ab

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14411838

Change-Id: I7e236435a71bdfe1d74eb4c5428284892aceeb96
This commit is contained in:
Kohsuke Yatoh
2021-05-04 16:50:59 +00:00
committed by Automerger Merge Worker
4 changed files with 158 additions and 77 deletions

View File

@@ -21,16 +21,15 @@ package {
default_applicable_licenses: ["frameworks_base_license"], default_applicable_licenses: ["frameworks_base_license"],
} }
java_test_host { android_test {
name: "UpdatableSystemFontTest", name: "UpdatableSystemFontTest",
srcs: ["src/**/*.java"], srcs: ["src/**/*.java"],
libs: [ libs: ["android.test.runner"],
"tradefed",
"compatibility-tradefed",
"compatibility-host-util",
],
static_libs: [ static_libs: [
"frameworks-base-hostutils", "androidx.test.ext.junit",
"compatibility-device-util-axt",
"platform-test-annotations",
"truth-prebuilt",
], ],
test_suites: [ test_suites: [
"general-tests", "general-tests",
@@ -47,4 +46,5 @@ java_test_host {
":UpdatableSystemFontTestNotoColorEmojiVPlus2Ttf", ":UpdatableSystemFontTestNotoColorEmojiVPlus2Ttf",
":UpdatableSystemFontTestNotoColorEmojiVPlus2TtfFsvSig", ":UpdatableSystemFontTestNotoColorEmojiVPlus2TtfFsvSig",
], ],
sdk_version: "test_current",
} }

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2021 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.updatablesystemfont">
<application android:label="UpdatableSystemFontTest">
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:label="UpdatableSystemFontTest"
android:targetPackage="com.android.updatablesystemfont">
</instrumentation>
</manifest>

View File

@@ -21,6 +21,7 @@
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" /> <option name="cleanup-apks" value="true" />
<option name="test-file-name" value="UpdatableSystemFontTest.apk" />
<option name="test-file-name" value="EmojiRenderingTestApp.apk" /> <option name="test-file-name" value="EmojiRenderingTestApp.apk" />
</target_preparer> </target_preparer>
@@ -37,7 +38,7 @@
<option name="push" value="UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig" /> <option name="push" value="UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig->/data/local/tmp/UpdatableSystemFontTestNotoColorEmojiVPlus2.ttf.fsv_sig" />
</target_preparer> </target_preparer>
<test class="com.android.compatibility.common.tradefed.testtype.JarHostTest" > <test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="jar" value="UpdatableSystemFontTest.jar" /> <option name="package" value="com.android.updatablesystemfont" />
</test> </test>
</configuration> </configuration>

View File

@@ -17,26 +17,35 @@
package com.android.updatablesystemfont; package com.android.updatablesystemfont;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assume.assumeTrue;
import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.SECONDS;
import android.app.UiAutomation;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.RootPermissionTest; import android.platform.test.annotations.RootPermissionTest;
import android.security.FileIntegrityManager;
import android.util.Log;
import android.util.Pair;
import com.android.fsverity.AddFsVerityCertRule; import androidx.annotation.Nullable;
import com.android.tradefed.device.DeviceNotAvailableException; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.tradefed.log.LogUtil.CLog; import androidx.test.platform.app.InstrumentationRegistry;
import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; import com.android.compatibility.common.util.StreamUtil;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -44,9 +53,10 @@ import java.util.regex.Pattern;
* Tests if fonts can be updated by 'cmd font'. * Tests if fonts can be updated by 'cmd font'.
*/ */
@RootPermissionTest @RootPermissionTest
@RunWith(DeviceJUnit4ClassRunner.class) @RunWith(AndroidJUnit4.class)
public class UpdatableSystemFontTest extends BaseHostJUnit4Test { public class UpdatableSystemFontTest {
private static final String TAG = "UpdatableSystemFontTest";
private static final String SYSTEM_FONTS_DIR = "/system/fonts/"; private static final String SYSTEM_FONTS_DIR = "/system/fonts/";
private static final String DATA_FONTS_DIR = "/data/fonts/files/"; private static final String DATA_FONTS_DIR = "/data/fonts/files/";
@@ -84,58 +94,65 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
T get() throws Exception; T get() throws Exception;
} }
@Rule private String mKeyId;
public final AddFsVerityCertRule mAddFsverityCertRule =
new AddFsVerityCertRule(this, CERT_PATH);
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
expectRemoteCommandToSucceed("cmd font clear"); Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Run tests only if updatable system font is enabled.
FileIntegrityManager fim = context.getSystemService(FileIntegrityManager.class);
assumeTrue(fim != null);
assumeTrue(fim.isApkVeritySupported());
mKeyId = insertCert(CERT_PATH);
expectCommandToSucceed("cmd font clear");
} }
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
expectRemoteCommandToSucceed("cmd font clear"); expectCommandToSucceed("cmd font clear");
if (mKeyId != null) {
expectCommandToSucceed("mini-keyctl unlink " + mKeyId + " .fs-verity");
}
} }
@Test @Test
public void updateFont() throws Exception { public void updateFont() throws Exception {
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(fontPath).startsWith(DATA_FONTS_DIR); assertThat(fontPath).startsWith(DATA_FONTS_DIR);
// The updated font should be readable and unmodifiable. // The updated font should be readable and unmodifiable.
expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null"); expectCommandToSucceed("dd status=none if=" + fontPath + " of=/dev/null");
expectRemoteCommandToFail("echo -n '' >> " + fontPath); expectCommandToFail("dd status=none if=" + CERT_PATH + " of=" + fontPath);
} }
@Test @Test
public void updateFont_twice() throws Exception { public void updateFont_twice() throws Exception {
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(fontPath2).startsWith(DATA_FONTS_DIR); assertThat(fontPath2).startsWith(DATA_FONTS_DIR);
assertThat(fontPath2).isNotEqualTo(fontPath); assertThat(fontPath2).isNotEqualTo(fontPath);
// The new file should be readable. // The new file should be readable.
expectRemoteCommandToSucceed("cat " + fontPath2 + " > /dev/null"); expectCommandToSucceed("dd status=none if=" + fontPath2 + " of=/dev/null");
// The old file should be still readable. // The old file should be still readable.
expectRemoteCommandToSucceed("cat " + fontPath + " > /dev/null"); expectCommandToSucceed("dd status=none if=" + fontPath + " of=/dev/null");
} }
@Test @Test
public void updateFont_allowSameVersion() throws Exception { public void updateFont_allowSameVersion() throws Exception {
// Update original font to the same version // Update original font to the same version
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
ORIGINAL_NOTO_COLOR_EMOJI_TTF, ORIGINAL_NOTO_COLOR_EMOJI_TTF_FSV_SIG)); ORIGINAL_NOTO_COLOR_EMOJI_TTF, ORIGINAL_NOTO_COLOR_EMOJI_TTF_FSV_SIG));
String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath2 = getFontPath(NOTO_COLOR_EMOJI_TTF);
// Update updated font to the same version // Update updated font to the same version
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
String fontPath3 = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath3 = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(fontPath).startsWith(DATA_FONTS_DIR); assertThat(fontPath).startsWith(DATA_FONTS_DIR);
@@ -147,21 +164,21 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
@Test @Test
public void updateFont_invalidCert() throws Exception { public void updateFont_invalidCert() throws Exception {
expectRemoteCommandToFail(String.format("cmd font update %s %s", expectCommandToFail(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
} }
@Test @Test
public void updateFont_downgradeFromSystem() throws Exception { public void updateFont_downgradeFromSystem() throws Exception {
expectRemoteCommandToFail(String.format("cmd font update %s %s", expectCommandToFail(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_V0_TTF, TEST_NOTO_COLOR_EMOJI_V0_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_V0_TTF, TEST_NOTO_COLOR_EMOJI_V0_TTF_FSV_SIG));
} }
@Test @Test
public void updateFont_downgradeFromData() throws Exception { public void updateFont_downgradeFromData() throws Exception {
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS2_TTF_FSV_SIG));
expectRemoteCommandToFail(String.format("cmd font update %s %s", expectCommandToFail(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
} }
@@ -178,7 +195,7 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
public void launchApp_afterUpdateFont() throws Exception { public void launchApp_afterUpdateFont() throws Exception {
String originalFontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); String originalFontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(originalFontPath).startsWith(SYSTEM_FONTS_DIR); assertThat(originalFontPath).startsWith(SYSTEM_FONTS_DIR);
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
String updatedFontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); String updatedFontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(updatedFontPath).startsWith(DATA_FONTS_DIR); assertThat(updatedFontPath).startsWith(DATA_FONTS_DIR);
@@ -191,57 +208,99 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
@Test @Test
public void reboot() throws Exception { public void reboot() throws Exception {
expectRemoteCommandToSucceed(String.format("cmd font update %s %s", expectCommandToSucceed(String.format("cmd font update %s %s",
TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG)); TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF, TEST_NOTO_COLOR_EMOJI_VPLUS1_TTF_FSV_SIG));
String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPath = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(fontPath).startsWith(DATA_FONTS_DIR); assertThat(fontPath).startsWith(DATA_FONTS_DIR);
// Emulate reboot by 'cmd font restart'. // Emulate reboot by 'cmd font restart'.
expectRemoteCommandToSucceed("cmd font restart"); expectCommandToSucceed("cmd font restart");
String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF); String fontPathAfterReboot = getFontPath(NOTO_COLOR_EMOJI_TTF);
assertThat(fontPathAfterReboot).isEqualTo(fontPath); assertThat(fontPathAfterReboot).isEqualTo(fontPath);
} }
private String getFontPath(String fontFileName) throws Exception { private static String insertCert(String certPath) throws Exception {
Pair<String, String> result;
try (InputStream is = new FileInputStream(certPath)) {
result = runShellCommand("mini-keyctl padd asymmetric fsv_test .fs-verity", is);
}
// Assert that there are no errors.
assertThat(result.second).isEmpty();
String keyId = result.first.trim();
assertThat(keyId).matches("^\\d+$");
return keyId;
}
private static String getFontPath(String fontFileName) throws Exception {
// TODO: add a dedicated command for testing. // TODO: add a dedicated command for testing.
String lines = expectRemoteCommandToSucceed("cmd font dump"); String lines = expectCommandToSucceed("cmd font dump");
for (String line : lines.split("\n")) { for (String line : lines.split("\n")) {
Matcher m = PATTERN_FONT.matcher(line); Matcher m = PATTERN_FONT.matcher(line);
if (m.find() && m.group(1).endsWith(fontFileName)) { if (m.find() && m.group(1).endsWith(fontFileName)) {
return m.group(1); return m.group(1);
} }
} }
CLog.e("Font not found: " + fontFileName); throw new AssertionError("Font not found: " + fontFileName);
return null;
} }
private void startActivity(String appId, String activityId) throws Exception { private static void startActivity(String appId, String activityId) throws Exception {
// Make sure that the app is installed and enabled. // Make sure that the app is installed and enabled.
waitUntil(ACTIVITY_TIMEOUT_MILLIS, () -> { waitUntil(ACTIVITY_TIMEOUT_MILLIS, () -> {
String packageInfo = expectRemoteCommandToSucceed( String packageInfo = expectCommandToSucceed("pm list packages -e " + appId);
"pm list packages -e " + EMOJI_RENDERING_TEST_APP_ID);
return !packageInfo.isEmpty(); return !packageInfo.isEmpty();
}); });
expectRemoteCommandToSucceed("am force-stop " + EMOJI_RENDERING_TEST_APP_ID); expectCommandToSucceed("am force-stop " + appId);
expectRemoteCommandToSucceed("am start-activity -n " + EMOJI_RENDERING_TEST_ACTIVITY); expectCommandToSucceed("am start-activity -n " + activityId);
} }
private String expectRemoteCommandToSucceed(String cmd) throws Exception { private static String expectCommandToSucceed(String cmd) throws IOException {
CommandResult result = getDevice().executeShellV2Command(cmd); Pair<String, String> result = runShellCommand(cmd, null);
assertWithMessage("`" + cmd + "` failed: " + result.getStderr()) // UiAutomation.runShellCommand() does not return exit code.
.that(result.getStatus()) // Assume that the command fails if stderr is not empty.
.isEqualTo(CommandStatus.SUCCESS); assertThat(result.second.trim()).isEmpty();
return result.getStdout(); return result.first;
} }
private void expectRemoteCommandToFail(String cmd) throws Exception { private static void expectCommandToFail(String cmd) throws IOException {
CommandResult result = getDevice().executeShellV2Command(cmd); Pair<String, String> result = runShellCommand(cmd, null);
assertWithMessage("Unexpected success from `" + cmd + "`: " + result.getStderr()) // UiAutomation.runShellCommand() does not return exit code.
.that(result.getStatus()) // Assume that the command fails if stderr is not empty.
.isNotEqualTo(CommandStatus.SUCCESS); assertThat(result.second.trim()).isNotEmpty();
} }
private void waitUntil(long timeoutMillis, ThrowingSupplier<Boolean> func) { /** Runs a command and returns (stdout, stderr). */
private static Pair<String, String> runShellCommand(String cmd, @Nullable InputStream input)
throws IOException {
Log.i(TAG, "runShellCommand: " + cmd);
UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
ParcelFileDescriptor[] rwe = automation.executeShellCommandRwe(cmd);
// executeShellCommandRwe returns [stdout, stdin, stderr].
try (ParcelFileDescriptor outFd = rwe[0];
ParcelFileDescriptor inFd = rwe[1];
ParcelFileDescriptor errFd = rwe[2]) {
if (input != null) {
try (OutputStream os = new FileOutputStream(inFd.getFileDescriptor())) {
StreamUtil.copyStreams(input, os);
}
}
// We have to close stdin before reading stdout and stderr.
// It's safe to close ParcelFileDescriptor multiple times.
inFd.close();
String stdout;
try (InputStream is = new FileInputStream(outFd.getFileDescriptor())) {
stdout = StreamUtil.readInputStream(is);
}
Log.i(TAG, "stdout = " + stdout);
String stderr;
try (InputStream is = new FileInputStream(errFd.getFileDescriptor())) {
stderr = StreamUtil.readInputStream(is);
}
Log.i(TAG, "stderr = " + stderr);
return new Pair<>(stdout, stderr);
}
}
private static void waitUntil(long timeoutMillis, ThrowingSupplier<Boolean> func) {
long untilMillis = System.currentTimeMillis() + timeoutMillis; long untilMillis = System.currentTimeMillis() + timeoutMillis;
do { do {
try { try {
@@ -256,25 +315,16 @@ public class UpdatableSystemFontTest extends BaseHostJUnit4Test {
throw new AssertionError("Timed out"); throw new AssertionError("Timed out");
} }
private boolean isFileOpenedBy(String path, String appId) throws DeviceNotAvailableException { private static boolean isFileOpenedBy(String path, String appId) throws Exception {
String pid = pidOf(appId); String pid = pidOf(appId);
if (pid.isEmpty()) { if (pid.isEmpty()) {
return false; return false;
} }
CommandResult result = getDevice().executeShellV2Command( String cmd = String.format("lsof -t -p %s %s", pid, path);
String.format("lsof -t -p %s '%s'", pid, path)); return !expectCommandToSucceed(cmd).trim().isEmpty();
if (result.getStatus() != CommandStatus.SUCCESS) {
return false;
}
// The file is open if the output of lsof is non-empty.
return !result.getStdout().trim().isEmpty();
} }
private String pidOf(String appId) throws DeviceNotAvailableException { private static String pidOf(String appId) throws Exception {
CommandResult result = getDevice().executeShellV2Command("pidof " + appId); return expectCommandToSucceed("pidof " + appId).trim();
if (result.getStatus() != CommandStatus.SUCCESS) {
return "";
}
return result.getStdout().trim();
} }
} }