Add a module generator preparer
Add a new preparer that can generate module configs at test runtime.
Bug: 175317761
Test: atest :presubmit
Test: make csuite && csuite-tradefed run simple-app-launch-test --package <package>
Change-Id: I3201c77edd3426eecafe1a92931dfe6ddd37cf2c
diff --git a/harness/Android.bp b/harness/Android.bp
index cea4d03..66f123b 100644
--- a/harness/Android.bp
+++ b/harness/Android.bp
@@ -23,6 +23,9 @@
libs: [
"tradefed",
],
+ static_libs: [
+ "compatibility-tradefed",
+ ]
}
java_test_host {
@@ -35,7 +38,9 @@
"tradefed",
],
static_libs: [
+ "compatibility-tradefed",
"guava-testlib",
+ "jimfs",
"mockito-host",
"objenesis",
"testng",
diff --git a/harness/src/main/java/com/android/csuite/core/GenerateModulePreparer.java b/harness/src/main/java/com/android/csuite/core/GenerateModulePreparer.java
new file mode 100644
index 0000000..33d1b85
--- /dev/null
+++ b/harness/src/main/java/com/android/csuite/core/GenerateModulePreparer.java
@@ -0,0 +1,166 @@
+/*
+ * 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.csuite.core;
+
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.Option.Importance;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Resources;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A preparer for generating TradeFed test suite modules config files.
+ *
+ * <p>This preparer generates module config files into TradeFed's test directory at runtime using a
+ * template. The entire test directory is delete before every run. As a result, there can only be
+ * one instance executing at a given time.
+ */
+public final class GenerateModulePreparer implements ITargetPreparer {
+
+ @VisibleForTesting static final String MODULE_FILE_EXTENSION = ".config";
+ @VisibleForTesting static final String OPTION_TEMPLATE = "template";
+ @VisibleForTesting static final String OPTION_PACKAGE = "package";
+ private static final String TEMPLATE_PACKAGE_PATTERN = "\\{package\\}";
+
+ @Option(
+ name = OPTION_TEMPLATE,
+ description = "Module config template resource path.",
+ importance = Importance.ALWAYS)
+ private String mTemplate;
+
+ @Option(name = OPTION_PACKAGE, description = "App package names.")
+ private final Set<String> mPackages = new HashSet<>();
+
+ private final TestDirectoryProvider mTestDirectoryProvider;
+ private final ResourceLoader mResourceLoader;
+ private final FileSystem mFileSystem;
+ private final List<Path> mGeneratedModules = new ArrayList<>();
+
+ public GenerateModulePreparer() {
+ this(FileSystems.getDefault());
+ }
+
+ private GenerateModulePreparer(FileSystem fileSystem) {
+ this(
+ fileSystem,
+ new CompatibilityTestDirectoryProvider(fileSystem),
+ new ClassResourceLoader());
+ }
+
+ @VisibleForTesting
+ GenerateModulePreparer(
+ FileSystem fileSystem,
+ TestDirectoryProvider testDirectoryProvider,
+ ResourceLoader resourceLoader) {
+ mFileSystem = fileSystem;
+ mTestDirectoryProvider = testDirectoryProvider;
+ mResourceLoader = resourceLoader;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(TestInformation testInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+ try {
+ Path testsDir = mTestDirectoryProvider.get(testInfo);
+ String templateContent = mResourceLoader.load(mTemplate);
+
+ for (String packageName : mPackages) {
+ validatePackageName(packageName);
+ Path modulePath = testsDir.resolve(packageName + MODULE_FILE_EXTENSION);
+ Files.write(
+ modulePath,
+ templateContent
+ .replaceAll(TEMPLATE_PACKAGE_PATTERN, packageName)
+ .getBytes());
+ mGeneratedModules.add(modulePath);
+ }
+ } catch (IOException e) {
+ throw new TargetSetupError("Failed to generate modules", e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
+ mGeneratedModules.forEach(
+ modulePath -> {
+ try {
+ Files.delete(modulePath);
+ } catch (IOException ioException) {
+ CLog.e("Failed to delete a generated module: " + modulePath, ioException);
+ }
+ });
+ }
+
+ private static void validatePackageName(String packageName) throws TargetSetupError {
+ if (packageName.isEmpty() || packageName.matches(".*" + TEMPLATE_PACKAGE_PATTERN + ".*")) {
+ throw new TargetSetupError(
+ "Package name cannot be empty or contains package placeholder: "
+ + TEMPLATE_PACKAGE_PATTERN);
+ }
+ }
+
+ @VisibleForTesting
+ interface ResourceLoader {
+ String load(String resourceName) throws IOException;
+ }
+
+ private static final class ClassResourceLoader implements ResourceLoader {
+ @Override
+ public String load(String resourceName) throws IOException {
+ return Resources.toString(
+ getClass().getClassLoader().getResource(resourceName), StandardCharsets.UTF_8);
+ }
+ }
+
+ @VisibleForTesting
+ interface TestDirectoryProvider {
+ Path get(TestInformation testInfo) throws IOException;
+ }
+
+ private static final class CompatibilityTestDirectoryProvider implements TestDirectoryProvider {
+ private final FileSystem mFileSystem;
+
+ private CompatibilityTestDirectoryProvider(FileSystem fileSystem) {
+ mFileSystem = fileSystem;
+ }
+
+ @Override
+ public Path get(TestInformation testInfo) throws IOException {
+ return mFileSystem.getPath(
+ new CompatibilityBuildHelper(testInfo.getBuildInfo()).getTestsDir().getPath());
+ }
+ }
+}
diff --git a/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java b/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
index e8aec05..01df8a6 100644
--- a/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
+++ b/harness/src/test/java/com/android/csuite/CSuiteUnitTests.java
@@ -26,6 +26,7 @@
com.android.compatibility.targetprep.SystemAppRemovalPreparerTest.class,
com.android.compatibility.testtype.AppLaunchTestTest.class,
com.android.csuite.config.AppRemoteFileResolverTest.class,
+ com.android.csuite.core.GenerateModulePreparerTest.class,
com.android.csuite.testing.CorrespondencesTest.class,
com.android.csuite.testing.MoreAssertsTest.class,
})
diff --git a/harness/src/test/java/com/android/csuite/core/GenerateModulePreparerTest.java b/harness/src/test/java/com/android/csuite/core/GenerateModulePreparerTest.java
new file mode 100644
index 0000000..eeaa2e2
--- /dev/null
+++ b/harness/src/test/java/com/android/csuite/core/GenerateModulePreparerTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.csuite.core;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.tradefed.config.OptionSetter;
+import com.android.tradefed.invoker.TestInformation;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import com.google.common.truth.IterableSubject;
+import com.google.common.truth.StringSubject;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@RunWith(JUnit4.class)
+public final class GenerateModulePreparerTest {
+ private static final String TEST_PACKAGE_NAME1 = "test.package.name1";
+ private static final String TEST_PACKAGE_NAME2 = "test.package.name2";
+ private static final String PACKAGE_PLACEHOLDER = "{package}";
+ private static final Exception NO_EXCEPTION = null;
+
+ private final FileSystem mFileSystem = Jimfs.newFileSystem(Configuration.unix());
+
+ @Test
+ public void tearDown_packageOptionIsSet_deletesGeneratedModules() throws Exception {
+ TestInformation testInfo = createTestInfo();
+ Path testsDir = createTestsDir();
+ GenerateModulePreparer preparer =
+ createPreparerBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME1) // Simulate duplicate package option
+ .addPackage(TEST_PACKAGE_NAME2)
+ .build();
+ preparer.setUp(testInfo);
+
+ preparer.tearDown(testInfo, NO_EXCEPTION);
+
+ assertThatListDirectory(testsDir).isEmpty();
+ }
+
+ @Test
+ public void tearDown_packageOptionIsNotSet_doesNotThrowError() throws Exception {
+ TestInformation testInfo = createTestInfo();
+ GenerateModulePreparer preparer =
+ createPreparerBuilder().setTestsDir(createTestsDir()).build();
+ preparer.setUp(testInfo);
+
+ preparer.tearDown(testInfo, NO_EXCEPTION);
+ }
+
+ @Test
+ public void setUp_packageNameIsEmptyString_throwsError() throws Exception {
+ GenerateModulePreparer preparer = createPreparerBuilder().addPackage("").build();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
+ }
+
+ @Test
+ public void setUp_packageNameContainsPlaceholder_throwsError() throws Exception {
+ GenerateModulePreparer preparer =
+ createPreparerBuilder().addPackage("a" + PACKAGE_PLACEHOLDER + "b").build();
+
+ assertThrows(TargetSetupError.class, () -> preparer.setUp(createTestInfo()));
+ }
+
+ @Test
+ public void setUp_packageOptionContainsDuplicates_ignoreDuplicates() throws Exception {
+ Path testsDir = createTestsDir();
+ GenerateModulePreparer preparer =
+ createPreparerBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME1) // Simulate duplicate package option
+ .addPackage(TEST_PACKAGE_NAME2)
+ .build();
+
+ preparer.setUp(createTestInfo());
+
+ assertThatListDirectory(testsDir)
+ .containsExactly(
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME1),
+ getModuleConfigFile(testsDir, TEST_PACKAGE_NAME2));
+ }
+
+ @Test
+ public void setUp_packageOptionNotSet_doesNotGenerate() throws Exception {
+ Path testsDir = createTestsDir();
+ GenerateModulePreparer preparer = createPreparerBuilder().setTestsDir(testsDir).build();
+
+ preparer.setUp(createTestInfo());
+
+ assertThatListDirectory(testsDir).isEmpty();
+ }
+
+ @Test
+ public void setUp_templateContainsPlaceholders_replacesPlaceholdersInOutput() throws Exception {
+ Path testsDir = createTestsDir();
+ String content = "hello placeholder%s%s world";
+ GenerateModulePreparer preparer =
+ createPreparerBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .setTemplateContent(
+ String.format(content, PACKAGE_PLACEHOLDER, PACKAGE_PLACEHOLDER))
+ .build();
+
+ preparer.setUp(createTestInfo());
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1)
+ .isEqualTo(String.format(content, TEST_PACKAGE_NAME1, TEST_PACKAGE_NAME1));
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2)
+ .isEqualTo(String.format(content, TEST_PACKAGE_NAME2, TEST_PACKAGE_NAME2));
+ }
+
+ @Test
+ public void setUp_templateDoesNotContainPlaceholder_outputsTemplateContent() throws Exception {
+ Path testsDir = createTestsDir();
+ String content = "no placeholder";
+ GenerateModulePreparer preparer =
+ createPreparerBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .setTemplateContent(content)
+ .build();
+
+ preparer.setUp(createTestInfo());
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1).isEqualTo(content);
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2).isEqualTo(content);
+ }
+
+ @Test
+ public void setUp_templateContentIsEmpty_outputsTemplateContent() throws Exception {
+ Path testsDir = createTestsDir();
+ String content = "";
+ GenerateModulePreparer preparer =
+ createPreparerBuilder()
+ .setTestsDir(testsDir)
+ .addPackage(TEST_PACKAGE_NAME1)
+ .addPackage(TEST_PACKAGE_NAME2)
+ .setTemplateContent(content)
+ .build();
+
+ preparer.setUp(createTestInfo());
+
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME1).isEqualTo(content);
+ assertThatModuleConfigFileContent(testsDir, TEST_PACKAGE_NAME2).isEqualTo(content);
+ }
+
+ private static StringSubject assertThatModuleConfigFileContent(
+ Path testsDir, String packageName) throws IOException {
+ return assertThat(
+ new String(Files.readAllBytes(getModuleConfigFile(testsDir, packageName))));
+ }
+
+ private static IterableSubject assertThatListDirectory(Path dir) throws IOException {
+ // Convert stream to list because com.google.common.truth.Truth8 is not available.
+ return assertThat(
+ Files.walk(dir)
+ .filter(p -> !p.equals(dir))
+ .collect(ImmutableList.toImmutableList()));
+ }
+
+ private static Path getModuleConfigFile(Path baseDir, String packageName) {
+ return baseDir.resolve(packageName + ".config");
+ }
+
+ private Path createTestsDir() throws IOException {
+ Path rootPath = mFileSystem.getPath("csuite");
+ Files.createDirectories(rootPath);
+ return Files.createTempDirectory(rootPath, "testDir");
+ }
+
+ private static TestInformation createTestInfo() {
+ return TestInformation.newBuilder().build();
+ }
+
+ private PreparerBuilder createPreparerBuilder() throws IOException {
+ return new PreparerBuilder()
+ .setFileSystem(mFileSystem)
+ .setTemplateContent(MODULE_TEMPLATE_CONTENT)
+ .setOption(GenerateModulePreparer.OPTION_TEMPLATE, "empty_path");
+ }
+
+ private static final class PreparerBuilder {
+ private final ListMultimap<String, String> mOptions = ArrayListMultimap.create();
+ private final List<String> mPackages = new ArrayList<>();
+ private Path mTestsDir;
+ private String mTemplateContent;
+ private FileSystem mFileSystem;
+
+ PreparerBuilder addPackage(String packageName) {
+ mPackages.add(packageName);
+ return this;
+ }
+
+ PreparerBuilder setFileSystem(FileSystem fileSystem) {
+ mFileSystem = fileSystem;
+ return this;
+ }
+
+ PreparerBuilder setTemplateContent(String templateContent) {
+ mTemplateContent = templateContent;
+ return this;
+ }
+
+ PreparerBuilder setTestsDir(Path testsDir) {
+ mTestsDir = testsDir;
+ return this;
+ }
+
+ PreparerBuilder setOption(String key, String value) {
+ mOptions.put(key, value);
+ return this;
+ }
+
+ GenerateModulePreparer build() throws Exception {
+ GenerateModulePreparer preparer =
+ new GenerateModulePreparer(
+ mFileSystem, testInfo -> mTestsDir, resourcePath -> mTemplateContent);
+
+ OptionSetter optionSetter = new OptionSetter(preparer);
+ for (Map.Entry<String, String> entry : mOptions.entries()) {
+ optionSetter.setOptionValue(entry.getKey(), entry.getValue());
+ }
+
+ for (String packageName : mPackages) {
+ optionSetter.setOptionValue(GenerateModulePreparer.OPTION_PACKAGE, packageName);
+ }
+
+ return preparer;
+ }
+ }
+
+ private static final String MODULE_TEMPLATE_CONTENT =
+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
+ + "<configuration description=\"description\">\n"
+ + " <option name=\"package-name\" value=\"{package}\"/>\n"
+ + " <target_preparer class=\"some.preparer.class\">\n"
+ + " <option name=\"test-file-name\" value=\"app://{package}\"/>\n"
+ + " </target_preparer>\n"
+ + " <test class=\"some.test.class\"/>\n"
+ + "</configuration>";
+}