1 /* 2 * Copyright (C) 2011 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.tools.lint.checks; 18 19 import com.android.tools.lint.LintCliXmlParser; 20 import com.android.tools.lint.LombokParser; 21 import com.android.tools.lint.Main; 22 import com.android.tools.lint.client.api.Configuration; 23 import com.android.tools.lint.client.api.IDomParser; 24 import com.android.tools.lint.client.api.IJavaParser; 25 import com.android.tools.lint.client.api.IssueRegistry; 26 import com.android.tools.lint.client.api.LintDriver; 27 import com.android.tools.lint.detector.api.Context; 28 import com.android.tools.lint.detector.api.Detector; 29 import com.android.tools.lint.detector.api.Issue; 30 import com.android.tools.lint.detector.api.Location; 31 import com.android.tools.lint.detector.api.Position; 32 import com.android.tools.lint.detector.api.Project; 33 import com.android.tools.lint.detector.api.Severity; 34 import com.google.common.io.Files; 35 import com.google.common.io.InputSupplier; 36 37 import java.io.File; 38 import java.io.FileWriter; 39 import java.io.IOException; 40 import java.io.InputStream; 41 import java.net.URISyntaxException; 42 import java.net.URL; 43 import java.security.CodeSource; 44 import java.util.ArrayList; 45 import java.util.Calendar; 46 import java.util.Collections; 47 import java.util.List; 48 49 import junit.framework.TestCase; 50 51 /** Common utility methods for the various lint check tests */ 52 @SuppressWarnings("javadoc") 53 public abstract class AbstractCheckTest extends TestCase { getDetector()54 protected abstract Detector getDetector(); 55 getIssues()56 protected List<Issue> getIssues() { 57 List<Issue> issues = new ArrayList<Issue>(); 58 Class<? extends Detector> detectorClass = getDetector().getClass(); 59 // Get the list of issues from the registry and filter out others, to make sure 60 // issues are properly registered 61 List<Issue> candidates = new BuiltinIssueRegistry().getIssues(); 62 for (Issue issue : candidates) { 63 if (issue.getDetectorClass() == detectorClass) { 64 issues.add(issue); 65 } 66 } 67 68 return issues; 69 } 70 71 private class CustomIssueRegistry extends IssueRegistry { 72 @Override getIssues()73 public List<Issue> getIssues() { 74 return AbstractCheckTest.this.getIssues(); 75 } 76 } 77 lintFiles(String... relativePaths)78 protected String lintFiles(String... relativePaths) throws Exception { 79 List<File> files = new ArrayList<File>(); 80 for (String relativePath : relativePaths) { 81 File file = getTestfile(relativePath); 82 assertNotNull(file); 83 files.add(file); 84 } 85 86 addManifestFile(getTargetDir()); 87 88 return checkLint(files); 89 } 90 checkLint(List<File> files)91 protected String checkLint(List<File> files) throws Exception { 92 mOutput = new StringBuilder(); 93 TestLintClient lintClient = new TestLintClient(); 94 LintDriver driver = new LintDriver(new CustomIssueRegistry(), lintClient); 95 driver.analyze(files, null /* scope */); 96 97 List<String> errors = lintClient.getErrors(); 98 Collections.sort(errors); 99 for (String error : errors) { 100 if (mOutput.length() > 0) { 101 mOutput.append('\n'); 102 } 103 mOutput.append(error); 104 } 105 106 if (mOutput.length() == 0) { 107 mOutput.append("No warnings."); 108 } 109 110 return mOutput.toString(); 111 } 112 113 /** 114 * Run lint on the given files when constructed as a separate project 115 * @return The output of the lint check. On Windows, this transforms all directory 116 * separators to the unix-style forward slash. 117 */ lintProject(String... relativePaths)118 protected String lintProject(String... relativePaths) throws Exception { 119 assertFalse("getTargetDir must be overridden to make a unique directory", 120 getTargetDir().equals(getTempDir())); 121 122 File projectDir = getTargetDir(); 123 124 List<File> files = new ArrayList<File>(); 125 for (String relativePath : relativePaths) { 126 File file = getTestfile(relativePath); 127 assertNotNull(file); 128 files.add(file); 129 } 130 131 addManifestFile(projectDir); 132 133 String result = checkLint(Collections.singletonList(projectDir)); 134 // The output typically contains a few directory/filenames. 135 // On Windows we need to change the separators to the unix-style 136 // forward slash to make the test as OS-agnostic as possible. 137 if (File.separatorChar != '/') { 138 result = result.replace(File.separatorChar, '/'); 139 } 140 return result; 141 } 142 addManifestFile(File projectDir)143 private void addManifestFile(File projectDir) throws IOException { 144 // Ensure that there is at least a manifest file there to make it a valid project 145 // as far as Lint is concerned: 146 if (!new File(projectDir, "AndroidManifest.xml").exists()) { 147 File manifest = new File(projectDir, "AndroidManifest.xml"); 148 FileWriter fw = new FileWriter(manifest); 149 fw.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + 150 "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" + 151 " package=\"foo.bar2\"\n" + 152 " android:versionCode=\"1\"\n" + 153 " android:versionName=\"1.0\" >\n" + 154 "</manifest>\n"); 155 fw.close(); 156 } 157 } 158 159 private StringBuilder mOutput = null; 160 161 private static File sTempDir = null; getTempDir()162 protected File getTempDir() { 163 if (sTempDir == null) { 164 File base = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$ 165 String os = System.getProperty("os.name"); //$NON-NLS-1$ 166 if (os.startsWith("Mac OS")) { //$NON-NLS-1$ 167 base = new File("/tmp"); 168 } 169 Calendar c = Calendar.getInstance(); 170 String name = String.format("lintTests/%1$tF_%1$tT", c).replace(':', '-'); //$NON-NLS-1$ 171 File tmpDir = new File(base, name); 172 if (!tmpDir.exists() && tmpDir.mkdirs()) { 173 sTempDir = tmpDir; 174 } else { 175 sTempDir = base; 176 } 177 } 178 179 return sTempDir; 180 } 181 getTargetDir()182 protected File getTargetDir() { 183 return new File(getTempDir(), getClass().getSimpleName() + "_" + getName()); 184 } 185 makeTestFile(String name, String relative, final InputStream contents)186 private File makeTestFile(String name, String relative, 187 final InputStream contents) throws IOException { 188 File dir = getTargetDir(); 189 if (relative != null) { 190 dir = new File(dir, relative); 191 if (!dir.exists()) { 192 boolean mkdir = dir.mkdirs(); 193 assertTrue(dir.getPath(), mkdir); 194 } 195 } else if (!dir.exists()) { 196 boolean mkdir = dir.mkdirs(); 197 assertTrue(dir.getPath(), mkdir); 198 } 199 File tempFile = new File(dir, name); 200 if (tempFile.exists()) { 201 tempFile.delete(); 202 } 203 204 Files.copy(new InputSupplier<InputStream>() { 205 public InputStream getInput() throws IOException { 206 return contents; 207 } 208 }, tempFile); 209 210 return tempFile; 211 } 212 getTestfile(String relativePath)213 private File getTestfile(String relativePath) throws IOException { 214 // Support replacing filenames and paths with a => syntax, e.g. 215 // dir/file.txt=>dir2/dir3/file2.java 216 // will read dir/file.txt from the test data and write it into the target 217 // directory as dir2/dir3/file2.java 218 219 String targetPath = relativePath; 220 int replaceIndex = relativePath.indexOf("=>"); //$NON-NLS-1$ 221 if (replaceIndex != -1) { 222 // foo=>bar 223 targetPath = relativePath.substring(replaceIndex + "=>".length()); 224 relativePath = relativePath.substring(0, replaceIndex); 225 } 226 227 String path = "data" + File.separator + relativePath; //$NON-NLS-1$ 228 InputStream stream = 229 AbstractCheckTest.class.getResourceAsStream(path); 230 assertNotNull(relativePath + " does not exist", stream); 231 int index = targetPath.lastIndexOf('/'); 232 String relative = null; 233 String name = targetPath; 234 if (index != -1) { 235 name = targetPath.substring(index + 1); 236 relative = targetPath.substring(0, index); 237 } 238 239 return makeTestFile(name, relative, stream); 240 } 241 isEnabled(Issue issue)242 protected boolean isEnabled(Issue issue) { 243 Class<? extends Detector> detectorClass = getDetector().getClass(); 244 if (issue.getDetectorClass() == detectorClass) { 245 return true; 246 } 247 248 return false; 249 } 250 includeParentPath()251 protected boolean includeParentPath() { 252 return false; 253 } 254 255 public class TestLintClient extends Main { 256 private List<String> mErrors = new ArrayList<String>(); 257 getErrors()258 public List<String> getErrors() { 259 return mErrors; 260 } 261 262 @Override report(Context context, Issue issue, Severity severity, Location location, String message, Object data)263 public void report(Context context, Issue issue, Severity severity, Location location, 264 String message, Object data) { 265 StringBuilder sb = new StringBuilder(); 266 267 if (issue == IssueRegistry.LINT_ERROR) { 268 return; 269 } 270 271 if (location != null && location.getFile() != null) { 272 // Include parent directory for locations that have alternates, since 273 // frequently the file name is the same across different resource folders 274 // and we want to make sure in the tests that we're indeed passing the 275 // right files in as secondary locations 276 if (location.getSecondary() != null || includeParentPath()) { 277 sb.append(location.getFile().getParentFile().getName() + "/" 278 + location.getFile().getName()); 279 } else { 280 sb.append(location.getFile().getName()); 281 } 282 283 sb.append(':'); 284 285 Position startPosition = location.getStart(); 286 if (startPosition != null) { 287 int line = startPosition.getLine(); 288 if (line >= 0) { 289 // line is 0-based, should display 1-based 290 sb.append(Integer.toString(line + 1)); 291 sb.append(':'); 292 } 293 } 294 295 sb.append(' '); 296 } 297 298 if (severity == Severity.FATAL) { 299 // Treat fatal errors like errors in the golden files. 300 severity = Severity.ERROR; 301 } 302 sb.append(severity.getDescription()); 303 sb.append(": "); 304 305 sb.append(message); 306 307 if (location != null && location.getSecondary() != null) { 308 location = location.getSecondary(); 309 while (location != null) { 310 if (location.getMessage() != null) { 311 sb.append('\n'); 312 sb.append("=> "); 313 sb.append(location.getFile().getParentFile().getName() + "/" 314 + location.getFile().getName()); 315 sb.append(':'); 316 Position startPosition = location.getStart(); 317 if (startPosition != null) { 318 int line = startPosition.getLine(); 319 if (line >= 0) { 320 // line is 0-based, should display 1-based 321 sb.append(Integer.toString(line + 1)); 322 sb.append(':'); 323 } 324 } 325 sb.append(' '); 326 if (location.getMessage() != null) { 327 sb.append(location.getMessage()); 328 } 329 } 330 location = location.getSecondary(); 331 } 332 } 333 mErrors.add(sb.toString()); 334 } 335 336 @Override log(Throwable exception, String format, Object... args)337 public void log(Throwable exception, String format, Object... args) { 338 if (exception != null) { 339 exception.printStackTrace(); 340 } 341 StringBuilder sb = new StringBuilder(); 342 if (format != null) { 343 sb.append(String.format(format, args)); 344 } 345 if (exception != null) { 346 sb.append(exception.toString()); 347 } 348 System.err.println(sb); 349 } 350 351 @Override getDomParser()352 public IDomParser getDomParser() { 353 return new LintCliXmlParser(); 354 } 355 356 @Override getJavaParser()357 public IJavaParser getJavaParser() { 358 return new LombokParser(); 359 } 360 361 @Override getConfiguration(Project project)362 public Configuration getConfiguration(Project project) { 363 return new TestConfiguration(); 364 } 365 366 @Override findResource(String relativePath)367 public File findResource(String relativePath) { 368 if (relativePath.equals("platform-tools/api/api-versions.xml")) { 369 CodeSource source = getClass().getProtectionDomain().getCodeSource(); 370 if (source != null) { 371 URL location = source.getLocation(); 372 try { 373 File dir = new File(location.toURI()); 374 assertTrue(dir.getPath(), dir.exists()); 375 File sdkDir = dir.getParentFile().getParentFile().getParentFile() 376 .getParentFile().getParentFile().getParentFile(); 377 File file = new File(sdkDir, "development" + File.separator + "sdk" 378 + File.separator + "api-versions.xml"); 379 return file; 380 } catch (URISyntaxException e) { 381 fail(e.getLocalizedMessage()); 382 } 383 } 384 } else { 385 fail("Unit tests don't support arbitrary resource lookup yet."); 386 } 387 388 return super.findResource(relativePath); 389 } 390 } 391 392 public class TestConfiguration extends Configuration { 393 @Override isEnabled(Issue issue)394 public boolean isEnabled(Issue issue) { 395 return AbstractCheckTest.this.isEnabled(issue); 396 } 397 398 @Override ignore(Context context, Issue issue, Location location, String message, Object data)399 public void ignore(Context context, Issue issue, Location location, String message, 400 Object data) { 401 fail("Not supported in tests."); 402 } 403 404 @Override setSeverity(Issue issue, Severity severity)405 public void setSeverity(Issue issue, Severity severity) { 406 fail("Not supported in tests."); 407 } 408 } 409 } 410