1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.csuite.config;
18 
19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
20 import com.android.csuite.core.PackageNameProvider;
21 import com.android.tradefed.build.IBuildInfo;
22 import com.android.tradefed.config.IConfiguration;
23 import com.android.tradefed.config.IConfigurationReceiver;
24 import com.android.tradefed.config.Option;
25 import com.android.tradefed.config.Option.Importance;
26 import com.android.tradefed.device.DeviceNotAvailableException;
27 import com.android.tradefed.invoker.TestInformation;
28 import com.android.tradefed.log.LogUtil.CLog;
29 import com.android.tradefed.result.ITestInvocationListener;
30 import com.android.tradefed.targetprep.ITargetPreparer;
31 import com.android.tradefed.testtype.IBuildReceiver;
32 import com.android.tradefed.testtype.IRemoteTest;
33 import com.android.tradefed.testtype.IShardableTest;
34 
35 import com.google.common.annotations.VisibleForTesting;
36 import com.google.common.io.Resources;
37 
38 import java.io.IOException;
39 import java.io.UncheckedIOException;
40 import java.nio.charset.StandardCharsets;
41 import java.nio.file.FileSystem;
42 import java.nio.file.FileSystems;
43 import java.nio.file.Files;
44 import java.nio.file.Path;
45 import java.util.Collection;
46 import java.util.HashSet;
47 import java.util.Set;
48 
49 /**
50  * A tool for generating TradeFed suite modules during runtime.
51  *
52  * <p>This class generates module config files into TradeFed's test directory at runtime using a
53  * template. Since the content of the test directory relies on what is being generated in a test
54  * run, there can only be one instance executing at a given time.
55  *
56  * <p>The intention of this class is to generate test modules at the beginning of a test run and
57  * cleans up after all tests finish, which resembles a target preparer. However, a target preparer
58  * is executed after the sharding process has finished. The only way to make the generated modules
59  * available for sharding without making changes to TradeFed's core code is to disguise this module
60  * generator as an instance of IShardableTest and declare it separately in test plan config. This is
61  * hacky, and in the long term a TradeFed centered solution is desired. For more details, see
62  * go/sharding-hack-for-module-gen. Note that since the generate step is executed as a test instance
63  * and cleanup step is executed as a target preparer, there should be no saved states between
64  * generating and cleaning up module files.
65  *
66  * <p>This module generator collects package names from all PackageNameProvider objects specified in
67  * the test configs.
68  *
69  * <h2>Syntax and usage</h2>
70  *
71  * <p>References to package name providers in TradeFed test configs must have the following syntax:
72  *
73  * <blockquote>
74  *
75  * <b>&lt;object type="PACKAGE_NAME_PROVIDER" class="</b><i>provider_class_name</i><b>"/&gt;</b>
76  *
77  * </blockquote>
78  *
79  * where <i>provider_class_name</i> is the fully-qualified class name of an PackageNameProvider
80  * implementation class.
81  */
82 public final class ModuleGenerator
83         implements IRemoteTest,
84                 IShardableTest,
85                 IBuildReceiver,
86                 ITargetPreparer,
87                 IConfigurationReceiver {
88 
89     @VisibleForTesting static final String MODULE_FILE_EXTENSION = ".config";
90     @VisibleForTesting static final String OPTION_TEMPLATE = "template";
91     @VisibleForTesting static final String PACKAGE_NAME_PROVIDER = "PACKAGE_NAME_PROVIDER";
92     private static final String TEMPLATE_PACKAGE_PATTERN = "\\{package\\}";
93     private static final Collection<IRemoteTest> NOT_SPLITABLE = null;
94 
95     @Option(
96             name = OPTION_TEMPLATE,
97             description = "Module config template resource path.",
98             importance = Importance.ALWAYS)
99     private String mTemplate;
100 
101     private final TestDirectoryProvider mTestDirectoryProvider;
102     private final ResourceLoader mResourceLoader;
103     private final FileSystem mFileSystem;
104     private IBuildInfo mBuildInfo;
105     private IConfiguration mConfiguration;
106 
107     @Override
setConfiguration(IConfiguration configuration)108     public void setConfiguration(IConfiguration configuration) {
109         mConfiguration = configuration;
110     }
111 
ModuleGenerator()112     public ModuleGenerator() {
113         this(FileSystems.getDefault());
114     }
115 
ModuleGenerator(FileSystem fileSystem)116     private ModuleGenerator(FileSystem fileSystem) {
117         this(
118                 fileSystem,
119                 new CompatibilityTestDirectoryProvider(fileSystem),
120                 new ClassResourceLoader());
121     }
122 
123     @VisibleForTesting
ModuleGenerator( FileSystem fileSystem, TestDirectoryProvider testDirectoryProvider, ResourceLoader resourceLoader)124     ModuleGenerator(
125             FileSystem fileSystem,
126             TestDirectoryProvider testDirectoryProvider,
127             ResourceLoader resourceLoader) {
128         mFileSystem = fileSystem;
129         mTestDirectoryProvider = testDirectoryProvider;
130         mResourceLoader = resourceLoader;
131     }
132 
133     @Override
run(final TestInformation testInfo, final ITestInvocationListener listener)134     public void run(final TestInformation testInfo, final ITestInvocationListener listener) {
135         // Intentionally left blank since this class is not really a test.
136     }
137 
138     @Override
setUp(TestInformation testInfo)139     public void setUp(TestInformation testInfo) {
140         // Intentionally left blank.
141     }
142 
143     @Override
setBuild(IBuildInfo buildInfo)144     public void setBuild(IBuildInfo buildInfo) {
145         mBuildInfo = buildInfo;
146     }
147 
148     /**
149      * Generates test modules. Note that the implementation of this method is not related to
150      * sharding in any way.
151      */
152     @Override
split()153     public Collection<IRemoteTest> split() {
154         try {
155             // Executes the generate step.
156             generateModules();
157         } catch (IOException e) {
158             throw new UncheckedIOException("Failed to generate modules", e);
159         }
160 
161         return NOT_SPLITABLE;
162     }
163 
164     /** Cleans up generated test modules. */
165     @Override
tearDown(TestInformation testInfo, Throwable e)166     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
167         // Gets build info from test info as when the class is executed as a ITargetPreparer
168         // preparer, it is not considered as a IBuildReceiver instance.
169         mBuildInfo = testInfo.getBuildInfo();
170 
171         try {
172             // Executes the clean up step.
173             cleanUpModules();
174         } catch (IOException ioException) {
175             throw new UncheckedIOException("Failed to clean up generated modules", ioException);
176         }
177     }
178 
getPackageNames()179     private Set<String> getPackageNames() throws IOException {
180         Set<String> packages = new HashSet<>();
181         for (Object provider : mConfiguration.getConfigurationObjectList(PACKAGE_NAME_PROVIDER)) {
182             packages.addAll(((PackageNameProvider) provider).get());
183         }
184         return packages;
185     }
186 
generateModules()187     private void generateModules() throws IOException {
188         String templateContent = mResourceLoader.load(mTemplate);
189 
190         for (String packageName : getPackageNames()) {
191             validatePackageName(packageName);
192             Files.write(
193                     getModulePath(packageName),
194                     templateContent.replaceAll(TEMPLATE_PACKAGE_PATTERN, packageName).getBytes());
195         }
196     }
197 
cleanUpModules()198     private void cleanUpModules() throws IOException {
199         getPackageNames()
200                 .forEach(
201                         packageName -> {
202                             try {
203                                 Files.delete(getModulePath(packageName));
204                             } catch (IOException ioException) {
205                                 CLog.e(
206                                         "Failed to delete the generated module for package "
207                                                 + packageName,
208                                         ioException);
209                             }
210                         });
211     }
212 
getModulePath(String packageName)213     private Path getModulePath(String packageName) throws IOException {
214         Path testsDir = mTestDirectoryProvider.get(mBuildInfo);
215         return testsDir.resolve(packageName + MODULE_FILE_EXTENSION);
216     }
217 
validatePackageName(String packageName)218     private static void validatePackageName(String packageName) {
219         if (packageName.isEmpty() || packageName.matches(".*" + TEMPLATE_PACKAGE_PATTERN + ".*")) {
220             throw new IllegalArgumentException(
221                     "Package name cannot be empty or contains package placeholder: "
222                             + TEMPLATE_PACKAGE_PATTERN);
223         }
224     }
225 
226     @VisibleForTesting
227     interface ResourceLoader {
load(String resourceName)228         String load(String resourceName) throws IOException;
229     }
230 
231     private static final class ClassResourceLoader implements ResourceLoader {
232         @Override
load(String resourceName)233         public String load(String resourceName) throws IOException {
234             return Resources.toString(
235                     getClass().getClassLoader().getResource(resourceName), StandardCharsets.UTF_8);
236         }
237     }
238 
239     @VisibleForTesting
240     interface TestDirectoryProvider {
get(IBuildInfo buildInfo)241         Path get(IBuildInfo buildInfo) throws IOException;
242     }
243 
244     private static final class CompatibilityTestDirectoryProvider implements TestDirectoryProvider {
245         private final FileSystem mFileSystem;
246 
CompatibilityTestDirectoryProvider(FileSystem fileSystem)247         private CompatibilityTestDirectoryProvider(FileSystem fileSystem) {
248             mFileSystem = fileSystem;
249         }
250 
251         @Override
get(IBuildInfo buildInfo)252         public Path get(IBuildInfo buildInfo) throws IOException {
253             return mFileSystem.getPath(
254                     new CompatibilityBuildHelper(buildInfo).getTestsDir().getPath());
255         }
256     }
257 }
258