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