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><object type="MODULE_INFO_PROVIDER" class="</b><i>provider_class_name</i><b>"/></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