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 package com.android.csuite.config; 17 18 import static com.android.csuite.testing.Correspondences.instanceOf; 19 import static com.android.csuite.testing.MoreAsserts.assertThrows; 20 21 import static com.google.common.truth.Truth.assertThat; 22 23 import static java.nio.charset.StandardCharsets.UTF_8; 24 25 import com.android.tradefed.build.BuildRetrievalError; 26 import com.android.tradefed.config.ConfigurationException; 27 import com.android.tradefed.config.Option; 28 import com.android.tradefed.config.OptionSetter; 29 import com.android.tradefed.config.remote.IRemoteFileResolver; 30 31 import com.google.common.collect.ImmutableList; 32 import com.google.common.collect.ImmutableMap; 33 import com.google.common.testing.NullPointerTester; 34 35 import org.junit.Rule; 36 import org.junit.Test; 37 import org.junit.rules.TemporaryFolder; 38 import org.junit.runner.RunWith; 39 import org.junit.runners.JUnit4; 40 41 import java.io.File; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.OutputStreamWriter; 45 import java.io.PrintWriter; 46 import java.net.URL; 47 import java.net.URLClassLoader; 48 import java.util.ServiceLoader; 49 import java.util.jar.JarEntry; 50 import java.util.jar.JarOutputStream; 51 52 @RunWith(JUnit4.class) 53 public final class AppRemoteFileResolverTest { 54 55 private static final String PACKAGE_NAME = "com.example.app"; 56 private static final File APP_URI_FILE = uriToFile("app://" + PACKAGE_NAME); 57 private static final ImmutableMap<String, String> EMPTY_PARAMS = ImmutableMap.of(); 58 59 @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); 60 61 // Class sanity tests. 62 63 @Test isServiceLoadable()64 public void isServiceLoadable() throws Exception { 65 ClassLoader classLoader = classLoaderWithProviders(AppRemoteFileResolver.class.getName()); 66 67 ServiceLoader<IRemoteFileResolver> serviceLoader = 68 ServiceLoader.load(IRemoteFileResolver.class, classLoader); 69 70 // Copy the list to provide better failure error messages since ServiceLoader's string 71 // representation is not very informative. 72 assertThat(ImmutableList.copyOf(serviceLoader)) 73 .comparingElementsUsing(instanceOf()) 74 .contains(AppRemoteFileResolver.class); 75 } 76 77 @Test nullPointers()78 public void nullPointers() { 79 NullPointerTester tester = new NullPointerTester(); 80 tester.setDefault(File.class, APP_URI_FILE); 81 tester.testAllPublicConstructors(AppRemoteFileResolver.class); 82 tester.testAllPublicInstanceMethods(new AppRemoteFileResolver()); 83 } 84 85 // URI validation tests. 86 87 @Test unsupportedUriScheme_throwsException()88 public void unsupportedUriScheme_throwsException() throws Exception { 89 AppRemoteFileResolver resolver = newResolverWithAnyTemplate(); 90 String uri = "gs://" + PACKAGE_NAME; 91 File f = uriToFile(uri); 92 93 Throwable thrown = 94 assertThrows( 95 IllegalArgumentException.class, 96 () -> resolver.resolveRemoteFiles(f, EMPTY_PARAMS)); 97 98 assertThat(thrown).hasMessageThat().contains("(gs)"); 99 assertThat(thrown).hasMessageThat().contains(uri); 100 } 101 102 @Test opaqueUri_throwsException()103 public void opaqueUri_throwsException() throws Exception { 104 AppRemoteFileResolver resolver = newResolverWithAnyTemplate(); 105 File uri = uriToFile("app:" + PACKAGE_NAME); 106 107 Throwable thrown = 108 assertThrows( 109 IllegalArgumentException.class, 110 () -> resolver.resolveRemoteFiles(uri, EMPTY_PARAMS)); 111 112 assertThat(thrown).hasMessageThat().contains("package name"); 113 } 114 115 @Test uriHasPathComponent_throwsException()116 public void uriHasPathComponent_throwsException() throws Exception { 117 AppRemoteFileResolver resolver = newResolverWithAnyTemplate(); 118 File uri = uriToFile("app://" + PACKAGE_NAME + "/invalid"); 119 120 Throwable thrown = 121 assertThrows( 122 IllegalArgumentException.class, 123 () -> resolver.resolveRemoteFiles(uri, EMPTY_PARAMS)); 124 125 assertThat(thrown).hasMessageThat().contains("invalid"); 126 } 127 128 // Template validation and expansion tests. 129 130 @Test templateNotSet_returnsNull()131 public void templateNotSet_returnsNull() throws Exception { 132 AppRemoteFileResolver resolver = new AppRemoteFileResolver(); 133 134 File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS); 135 136 assertThat(actual).isNull(); 137 } 138 139 @Test emptyTemplate_throwsException()140 public void emptyTemplate_throwsException() throws Exception { 141 AppRemoteFileResolver resolver = newResolverWithTemplate(""); 142 143 Throwable thrown = 144 assertThrows( 145 IllegalStateException.class, 146 () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS)); 147 148 assertThat(thrown).hasMessageThat().contains(AppRemoteFileResolver.URI_TEMPLATE_OPTION); 149 } 150 151 @Test templateHasNoPlaceholders_returnsFileWithoutExpansion()152 public void templateHasNoPlaceholders_returnsFileWithoutExpansion() throws Exception { 153 File expected = temporaryFolder.newFolder(); 154 AppRemoteFileResolver resolver = newResolverWithTemplate(expected.toURI().toString()); 155 156 File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS); 157 158 assertThat(actual).isEqualTo(expected); 159 } 160 161 @Test templateContainsPlaceholderForUndefinedVar_throwsException()162 public void templateContainsPlaceholderForUndefinedVar_throwsException() throws Exception { 163 AppRemoteFileResolver resolver = newResolverWithTemplate("file://{undefined}"); 164 165 Throwable thrown = 166 assertThrows( 167 IllegalStateException.class, 168 () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS)); 169 170 assertThat(thrown).hasMessageThat().contains("undefined"); 171 } 172 173 @Test templateExpandsToInvalidUri_throwsException()174 public void templateExpandsToInvalidUri_throwsException() throws Exception { 175 AppRemoteFileResolver resolver = newResolverWithTemplate("file:\\{package}"); 176 177 Throwable thrown = 178 assertThrows( 179 IllegalStateException.class, 180 () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS)); 181 182 assertThat(thrown).hasMessageThat().contains(AppRemoteFileResolver.URI_TEMPLATE_OPTION); 183 } 184 185 @Test templateContainsPlaceholder_resolvesUriToFile()186 public void templateContainsPlaceholder_resolvesUriToFile() throws Exception { 187 File parent = temporaryFolder.newFolder(); 188 File expected = new File(parent, PACKAGE_NAME); 189 String template = new File(parent, "{package}").toString(); 190 AppRemoteFileResolver resolver = newResolverWithTemplate(template); 191 File uri = uriToFile("app://" + PACKAGE_NAME); 192 193 File actual = resolver.resolveRemoteFiles(uri, EMPTY_PARAMS); 194 195 assertThat(actual).isEqualTo(expected); 196 } 197 198 @Test templateExpandsToAppUri_throwsException()199 public void templateExpandsToAppUri_throwsException() throws Exception { 200 AppRemoteFileResolver resolver = newResolverWithTemplate("app://{package}"); 201 202 Throwable thrown = 203 assertThrows( 204 BuildRetrievalError.class, 205 () -> resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS)); 206 207 assertThat(thrown).hasMessageThat().contains("'app'"); 208 } 209 210 @Test templateExpandsToUriWithUnsupportedScheme_returnsExpandedUri()211 public void templateExpandsToUriWithUnsupportedScheme_returnsExpandedUri() throws Exception { 212 String uri = "unsupported://" + PACKAGE_NAME; 213 AppRemoteFileResolver resolver = newResolverWithTemplate(uri); 214 File expected = uriToFile(uri); 215 216 File actual = resolver.resolveRemoteFiles(APP_URI_FILE, EMPTY_PARAMS); 217 218 assertThat(actual).isEqualTo(expected); 219 } 220 221 // Utility classes and methods. 222 223 /** 224 * Constructs a File from a URI string using the same logic TradeFed uses since it's tricky and 225 * has some gotchas such as stripping slashes. 226 */ uriToFile(String str)227 private static File uriToFile(String str) { 228 FileOptionSource optionSource = new FileOptionSource(); 229 230 try { 231 OptionSetter setter = new OptionSetter(optionSource); 232 setter.setOptionValue(FileOptionSource.OPTION_NAME, str); 233 } catch (ConfigurationException e) { 234 throw new RuntimeException(e); 235 } 236 237 return optionSource.file; 238 } 239 240 private static final class FileOptionSource { 241 static final String OPTION_NAME = "file"; 242 243 @Option(name = OPTION_NAME) 244 public File file; 245 } 246 newResolverWithAnyTemplate()247 private static AppRemoteFileResolver newResolverWithAnyTemplate() 248 throws ConfigurationException { 249 return newResolverWithTemplate("file:///tmp/{package}"); 250 } 251 newResolverWithTemplate(String uriTemplate)252 private static AppRemoteFileResolver newResolverWithTemplate(String uriTemplate) 253 throws ConfigurationException { 254 AppRemoteFileResolver resolver = new AppRemoteFileResolver(); 255 OptionSetter setter = new OptionSetter(resolver); 256 setter.setOptionValue("app:" + AppRemoteFileResolver.URI_TEMPLATE_OPTION, uriTemplate); 257 return resolver; 258 } 259 classLoaderWithProviders(String... lines)260 private ClassLoader classLoaderWithProviders(String... lines) throws IOException { 261 String service = IRemoteFileResolver.class.getName(); 262 File jar = temporaryFolder.newFile(); 263 264 try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar))) { 265 JarEntry jarEntry = new JarEntry("META-INF/services/" + service); 266 267 out.putNextEntry(jarEntry); 268 PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, UTF_8)); 269 270 for (String line : lines) { 271 writer.println(line); 272 } 273 274 writer.flush(); 275 } 276 277 return new URLClassLoader(new URL[] {jar.toURI().toURL()}); 278 } 279 } 280