• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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