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.core;
18 
19 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
20 import com.android.tradefed.build.IBuildInfo;
21 import com.android.tradefed.config.IConfiguration;
22 import com.android.tradefed.config.IConfigurationReceiver;
23 import com.android.tradefed.config.Option;
24 import com.android.tradefed.config.Option.Importance;
25 import com.android.tradefed.device.DeviceNotAvailableException;
26 import com.android.tradefed.invoker.TestInformation;
27 import com.android.tradefed.result.ITestInvocationListener;
28 import com.android.tradefed.targetprep.ITargetPreparer;
29 import com.android.tradefed.testtype.IBuildReceiver;
30 import com.android.tradefed.testtype.IRemoteTest;
31 import com.android.tradefed.testtype.IShardableTest;
32 
33 import com.google.common.annotations.VisibleForTesting;
34 import com.google.common.base.Preconditions;
35 import com.google.errorprone.annotations.MustBeClosed;
36 
37 import java.io.IOException;
38 import java.io.UncheckedIOException;
39 import java.nio.file.Files;
40 import java.nio.file.Path;
41 import java.util.Collection;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 import java.util.stream.Stream;
46 
47 /**
48  * Generates TradeFed suite modules during runtime.
49  *
50  * <p>This class generates module config files into TradeFed's test directory at runtime using a
51  * template. Since the content of the test directory relies on what is being generated in a test
52  * run, there can only be one instance executing at a given time.
53  *
54  * <p>The intention of this class is to generate test modules at the beginning of a test run and
55  * cleans up after all tests finish, which resembles a target preparer. However, a target preparer
56  * is executed after the sharding process has finished. The only way to make the generated modules
57  * available for sharding without making changes to TradeFed's core code is to disguise this module
58  * generator as an instance of IShardableTest and declare it separately in test plan config. This is
59  * hacky, and in the long term a TradeFed centered solution is desired. For more details, see
60  * go/sharding-hack-for-module-gen. Note that since the generate step is executed as a test instance
61  * and cleanup step is executed as a target preparer, there should be no saved states between
62  * generating and cleaning up module files.
63  *
64  * <p>This module generator collects modules' info from all ModuleInfoProvider objects specified in
65  * the test plan config.
66  *
67  * <h2>Syntax and usage</h2>
68  *
69  * <p>References to module info providers in TradeFed test plan config must have the following
70  * syntax:
71  *
72  * <blockquote>
73  *
74  * <b>&lt;object type="MODULE_INFO_PROVIDER" class="</b><i>provider_class_name</i><b>"/&gt;</b>
75  *
76  * </blockquote>
77  *
78  * where <i>provider_class_name</i> is the fully-qualified class name of an ModuleInfoProvider
79  * implementation class.
80  */
81 public final class ModuleGenerator
82         implements IRemoteTest,
83                 IShardableTest,
84                 IBuildReceiver,
85                 IConfigurationReceiver,
86                 ITargetPreparer {
87     @VisibleForTesting static final String MODULE_FILE_NAME_EXTENSION = ".config";
88 
89     @VisibleForTesting
90     static final String OPTION_PRESERVE_EXISTING_MODULES = "preserve-existing-modules";
91 
92     @Option(
93             name = OPTION_PRESERVE_EXISTING_MODULES,
94             description =
95                     "Whether to preserve non-generated modules existing in the csuite jar build. By"
96                         + " default, these modules will be deleted before test to prevent confusion"
97                         + " with the generated modules unless this option is set true.",
98             importance = Importance.NEVER)
99     private boolean mPreserveExistingModules = false;
100 
101     private static final Collection<IRemoteTest> NOT_SPLITTABLE = null;
102 
103     private final TestDirectoryProvider mTestDirectoryProvider;
104     private IBuildInfo mBuildInfo;
105     private IConfiguration mConfiguration;
106 
ModuleGenerator()107     public ModuleGenerator() {
108         this(buildInfo -> new CompatibilityBuildHelper(buildInfo).getTestsDir().toPath());
109     }
110 
111     @VisibleForTesting
ModuleGenerator(TestDirectoryProvider testDirectoryProvider)112     ModuleGenerator(TestDirectoryProvider testDirectoryProvider) {
113         mTestDirectoryProvider = testDirectoryProvider;
114     }
115 
116     @Override
setUp(TestInformation testInfo)117     public void setUp(TestInformation testInfo) {
118         // Do not add cleanup code here as this method is executed after the split method.
119     }
120 
121     /**
122      * Cleans up the generated test modules files.
123      *
124      * <p>Note that this method does not execute from the same instance of this class that generates
125      * the modules so be careful when using any class fields.
126      */
127     @Override
tearDown(TestInformation testInfo, Throwable e)128     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
129         // Gets build info from test info as when the class is executed as a ITargetPreparer
130         // preparer, it is not considered as a IBuildReceiver instance.
131         mBuildInfo = testInfo.getBuildInfo();
132 
133         try {
134             deleteModuleFiles();
135         } catch (IOException ioException) {
136             throw new UncheckedIOException(ioException);
137         }
138     }
139 
deleteModuleFiles()140     private void deleteModuleFiles() throws IOException {
141         Files.list(mTestDirectoryProvider.get(mBuildInfo))
142                 .filter(Files::isRegularFile)
143                 .filter(path -> path.toString().endsWith(MODULE_FILE_NAME_EXTENSION))
144                 .filter(
145                         path -> {
146                             if (!mPreserveExistingModules) {
147                                 return true;
148                             }
149                             try {
150                                 return Files.readString(path).contains(GENERATED_MODULE_NOTE);
151                             } catch (IOException ioException) {
152                                 throw new UncheckedIOException(ioException);
153                             }
154                         })
155                 .forEach(
156                         path -> {
157                             try {
158                                 Files.delete(path);
159                             } catch (IOException ioException) {
160                                 throw new UncheckedIOException(ioException);
161                             }
162                         });
163     }
164 
generateModules()165     private void generateModules() throws IOException {
166         deleteModuleFiles();
167         try (Stream<ModuleInfoProvider.ModuleInfo> modulesInfo = getModulesInfo()) {
168             Set<String> moduleNames = new HashSet<>();
169             modulesInfo.forEachOrdered(
170                     moduleInfo -> {
171                         String moduleName = moduleInfo.getName().trim();
172                         if (moduleName.isEmpty()) {
173                             throw new IllegalArgumentException("Module name cannot be empty.");
174                         }
175 
176                         if (moduleNames.contains(moduleName)) {
177                             throw new IllegalArgumentException(
178                                     "Duplicated module name: " + moduleName);
179                         }
180 
181                         try {
182                             Files.write(
183                                     getModulePath(moduleName),
184                                     (moduleInfo.getContent() + GENERATED_MODULE_NOTE).getBytes());
185                         } catch (IOException e) {
186                             throw new UncheckedIOException(e);
187                         }
188 
189                         moduleNames.add(moduleName);
190                     });
191         }
192     }
193 
194     @MustBeClosed
195     @SuppressWarnings("MustBeClosedChecker")
getModulesInfo()196     private Stream<ModuleInfoProvider.ModuleInfo> getModulesInfo() {
197         List<?> configurations =
198                 mConfiguration.getConfigurationObjectList(
199                         ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE);
200         Preconditions.checkNotNull(
201                 configurations, "Missing " + ModuleInfoProvider.MODULE_INFO_PROVIDER_OBJECT_TYPE);
202         return configurations.stream()
203                 .map(obj -> (ModuleInfoProvider) obj)
204                 .flatMap(
205                         info -> {
206                             try {
207                                 return info.get(mConfiguration);
208                             } catch (IOException ioException) {
209                                 throw new UncheckedIOException(ioException);
210                             }
211                         });
212     }
213 
214     /**
215      * Generates test modules. Note that the implementation of this method is not related to
216      * sharding in any way.
217      */
218     @Override
219     public Collection<IRemoteTest> split() {
220         try {
221             generateModules();
222         } catch (IOException ioException) {
223             throw new UncheckedIOException(ioException);
224         }
225         return NOT_SPLITTABLE;
226     }
227 
228     private Path getModulePath(String moduleName) throws IOException {
229         return mTestDirectoryProvider
230                 .get(mBuildInfo)
231                 .resolve(moduleName + MODULE_FILE_NAME_EXTENSION);
232     }
233 
234     @Override
235     public void run(final TestInformation testInfo, final ITestInvocationListener listener) {
236         // Intentionally left blank since this class is not really a test.
237     }
238 
239     @Override
240     public void setBuild(IBuildInfo buildInfo) {
241         mBuildInfo = buildInfo;
242     }
243 
244     @Override
245     public void setConfiguration(IConfiguration configuration) {
246         mConfiguration = configuration;
247     }
248 
249     @VisibleForTesting
250     interface TestDirectoryProvider {
251         Path get(IBuildInfo buildInfo) throws IOException;
252     }
253 
254     @VisibleForTesting
255     static final String GENERATED_MODULE_NOTE =
256             "<!-- Note: The content of this module is auto generated from a template. Please do"
257                     + " not modify manually. -->\n";
258 }
259