| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.testutils; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.google.common.base.Charsets; |
| import com.google.common.collect.Sets; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.io.Closeables; |
| import com.google.common.io.Files; |
| import com.google.common.io.InputSupplier; |
| |
| import junit.framework.TestCase; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * Common test case for SDK unit tests. Contains a number of general utility methods |
| * to help writing test cases, such as looking up a temporary directory, comparing golden |
| * files, computing string diffs, etc. |
| */ |
| @SuppressWarnings("javadoc") |
| public abstract class SdkTestCase extends TestCase { |
| /** Update golden files if different from the actual results */ |
| private static final boolean UPDATE_DIFFERENT_FILES = false; |
| /** Create golden files if missing */ |
| private static final boolean UPDATE_MISSING_FILES = true; |
| private static File sTempDir = null; |
| protected static Set<File> sCleanDirs = Sets.newHashSet(); |
| |
| protected String getTestDataRelPath() { |
| fail("Must be overridden"); |
| return null; |
| } |
| |
| public static int getCaretOffset(String fileContent, String caretLocation) { |
| assertTrue(caretLocation, caretLocation.contains("^")); //$NON-NLS-1$ |
| |
| int caretDelta = caretLocation.indexOf("^"); //$NON-NLS-1$ |
| assertTrue(caretLocation, caretDelta != -1); |
| |
| // String around caret/range without the range and caret marker characters |
| String caretContext; |
| if (caretLocation.contains("[^")) { //$NON-NLS-1$ |
| caretDelta--; |
| assertTrue(caretLocation, caretLocation.startsWith("[^", caretDelta)); //$NON-NLS-1$ |
| int caretRangeEnd = caretLocation.indexOf(']', caretDelta + 2); |
| assertTrue(caretLocation, caretRangeEnd != -1); |
| caretContext = caretLocation.substring(0, caretDelta) |
| + caretLocation.substring(caretDelta + 2, caretRangeEnd) |
| + caretLocation.substring(caretRangeEnd + 1); |
| } else { |
| caretContext = caretLocation.substring(0, caretDelta) |
| + caretLocation.substring(caretDelta + 1); // +1: skip "^" |
| } |
| |
| int caretContextIndex = fileContent.indexOf(caretContext); |
| assertTrue("Caret content " + caretContext + " not found in file", |
| caretContextIndex != -1); |
| return caretContextIndex + caretDelta; |
| } |
| |
| public static String addSelection(String newFileContents, int selectionBegin, int selectionEnd) { |
| // Insert selection markers -- [ ] for the selection range, ^ for the caret |
| String newFileWithCaret; |
| if (selectionBegin < selectionEnd) { |
| newFileWithCaret = newFileContents.substring(0, selectionBegin) + "[^" |
| + newFileContents.substring(selectionBegin, selectionEnd) + "]" |
| + newFileContents.substring(selectionEnd); |
| } else { |
| // Selected range |
| newFileWithCaret = newFileContents.substring(0, selectionBegin) + "^" |
| + newFileContents.substring(selectionBegin); |
| } |
| |
| return newFileWithCaret; |
| } |
| |
| public static String getCaretContext(String file, int offset) { |
| int windowSize = 20; |
| int begin = Math.max(0, offset - windowSize / 2); |
| int end = Math.min(file.length(), offset + windowSize / 2); |
| |
| return "..." + file.substring(begin, offset) + "^" + file.substring(offset, end) + "..."; |
| } |
| |
| /** Get the location to write missing golden files to */ |
| protected File getTargetDir() { |
| // Set $ADT_SDK_SOURCE_PATH to point to your git "sdk" directory; if done, then |
| // if you run a unit test which refers to a golden file which does not exist, it |
| // will be created directly into the test data directory and you can rerun the |
| // test |
| // and it should pass (after you verify that the golden file contains the correct |
| // result of course). |
| String sdk = System.getenv("ADT_SDK_SOURCE_PATH"); |
| if (sdk != null) { |
| File sdkPath = new File(sdk); |
| if (sdkPath.exists()) { |
| File testData = new File(sdkPath, getTestDataRelPath().replace('/', |
| File.separatorChar)); |
| if (testData.exists()) { |
| addCleanupDir(testData); |
| return testData; |
| } |
| } |
| } |
| return getTempDir(); |
| } |
| |
| public static File getTempDir() { |
| if (sTempDir == null) { |
| File base = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$ |
| if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { |
| base = new File("/tmp"); //$NON-NLS-1$ |
| } |
| |
| // On Windows, we don't want to pollute the temp folder (which is generally |
| // already incredibly busy). So let's create a temp folder for the results. |
| |
| Calendar c = Calendar.getInstance(); |
| String name = String.format("sdkTests_%1$tF_%1$tT", c).replace(':', '-'); //$NON-NLS-1$ |
| File tmpDir = new File(base, name); |
| if (!tmpDir.exists() && tmpDir.mkdir()) { |
| sTempDir = tmpDir; |
| } else { |
| sTempDir = base; |
| } |
| addCleanupDir(sTempDir); |
| } |
| |
| return sTempDir; |
| } |
| |
| protected String removeSessionData(String data) { |
| return data; |
| } |
| |
| protected InputStream getTestResource(String relativePath, boolean expectExists) { |
| String path = "testdata" + File.separator + relativePath; //$NON-NLS-1$ |
| InputStream stream = SdkTestCase.class.getResourceAsStream(path); |
| if (!expectExists && stream == null) { |
| return null; |
| } |
| return stream; |
| } |
| |
| @SuppressWarnings("resource") |
| protected String readTestFile(String relativePath, boolean expectExists) throws IOException { |
| InputStream stream = getTestResource(relativePath, expectExists); |
| if (expectExists) { |
| assertNotNull(relativePath + " does not exist", stream); |
| } else if (stream == null) { |
| return null; |
| } |
| |
| String xml = new String(ByteStreams.toByteArray(stream), Charsets.UTF_8); |
| try { |
| Closeables.close(stream, true /* swallowIOException */); |
| } catch (IOException e) { |
| // cannot happen |
| } |
| |
| assertTrue(xml.length() > 0); |
| |
| // Remove any references to the project name such that we are isolated from |
| // that in golden file. |
| // Appears in strings.xml etc. |
| xml = removeSessionData(xml); |
| |
| return xml; |
| } |
| |
| protected void assertEqualsGolden(String basename, String actual) throws IOException { |
| assertEqualsGolden(basename, actual, basename.substring(basename.lastIndexOf('.') + 1)); |
| } |
| |
| protected void assertEqualsGolden(String basename, String actual, String newExtension) |
| throws IOException { |
| String testName = getName(); |
| if (testName.startsWith("test")) { |
| testName = testName.substring(4); |
| if (Character.isUpperCase(testName.charAt(0))) { |
| testName = Character.toLowerCase(testName.charAt(0)) + testName.substring(1); |
| } |
| } |
| String expectedName; |
| String extension = basename.substring(basename.lastIndexOf('.') + 1); |
| if (newExtension == null) { |
| newExtension = extension; |
| } |
| expectedName = basename.substring(0, basename.indexOf('.')) |
| + "-expected-" + testName + '.' + newExtension; |
| String expected = readTestFile(expectedName, false); |
| if (expected == null) { |
| File expectedPath = new File( |
| UPDATE_MISSING_FILES ? getTargetDir() : getTempDir(), expectedName); |
| Files.write(actual, expectedPath, Charsets.UTF_8); |
| System.out.println("Expected - written to " + expectedPath + ":\n"); |
| System.out.println(actual); |
| fail("Did not find golden file (" + expectedName + "): Wrote contents as " |
| + expectedPath); |
| } else { |
| if (!expected.replaceAll("\r\n", "\n").equals(actual.replaceAll("\r\n", "\n"))) { |
| File expectedPath = new File(getTempDir(), expectedName); |
| File actualPath = new File(getTempDir(), |
| expectedName.replace("expected", "actual")); |
| Files.write(expected, expectedPath, Charsets.UTF_8); |
| Files.write(actual, actualPath, Charsets.UTF_8); |
| // Also update data dir with the current value |
| if (UPDATE_DIFFERENT_FILES) { |
| Files.write(actual, new File(getTargetDir(), expectedName), Charsets.UTF_8); |
| } |
| System.out.println("The files differ: diff " + expectedPath + " " |
| + actualPath); |
| assertEquals("The files differ - see " + expectedPath + " versus " + actualPath, |
| expected, actual); |
| } |
| } |
| } |
| |
| /** Creates a diff of two strings */ |
| public static String getDiff(String before, String after) { |
| return getDiff(before.split("\n"), after.split("\n")); |
| } |
| |
| public static String getDiff(String[] before, String[] after) { |
| // Based on the LCS section in http://introcs.cs.princeton.edu/java/96optimization/ |
| StringBuilder sb = new StringBuilder(); |
| |
| int n = before.length; |
| int m = after.length; |
| |
| // Compute longest common subsequence of x[i..m] and y[j..n] bottom up |
| int[][] lcs = new int[n + 1][m + 1]; |
| for (int i = n - 1; i >= 0; i--) { |
| for (int j = m - 1; j >= 0; j--) { |
| if (before[i].equals(after[j])) { |
| lcs[i][j] = lcs[i + 1][j + 1] + 1; |
| } else { |
| lcs[i][j] = Math.max(lcs[i + 1][j], lcs[i][j + 1]); |
| } |
| } |
| } |
| |
| int i = 0; |
| int j = 0; |
| while ((i < n) && (j < m)) { |
| if (before[i].equals(after[j])) { |
| i++; |
| j++; |
| } else { |
| sb.append("@@ -"); |
| sb.append(Integer.toString(i + 1)); |
| sb.append(" +"); |
| sb.append(Integer.toString(j + 1)); |
| sb.append('\n'); |
| while (i < n && j < m && !before[i].equals(after[j])) { |
| if (lcs[i + 1][j] >= lcs[i][j + 1]) { |
| sb.append('-'); |
| if (!before[i].trim().isEmpty()) { |
| sb.append(' '); |
| } |
| sb.append(before[i]); |
| sb.append('\n'); |
| i++; |
| } else { |
| sb.append('+'); |
| if (!after[j].trim().isEmpty()) { |
| sb.append(' '); |
| } |
| sb.append(after[j]); |
| sb.append('\n'); |
| j++; |
| } |
| } |
| } |
| } |
| |
| if (i < n || j < m) { |
| assert i == n || j == m; |
| sb.append("@@ -"); |
| sb.append(Integer.toString(i + 1)); |
| sb.append(" +"); |
| sb.append(Integer.toString(j + 1)); |
| sb.append('\n'); |
| for (; i < n; i++) { |
| sb.append('-'); |
| if (!before[i].trim().isEmpty()) { |
| sb.append(' '); |
| } |
| sb.append(before[i]); |
| sb.append('\n'); |
| } |
| for (; j < m; j++) { |
| sb.append('+'); |
| if (!after[j].trim().isEmpty()) { |
| sb.append(' '); |
| } |
| sb.append(after[j]); |
| sb.append('\n'); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| protected void deleteFile(File dir) { |
| TestUtils.deleteFile(dir); |
| } |
| |
| protected File makeTestFile(String name, String relative, |
| final InputStream contents) throws IOException { |
| return makeTestFile(getTargetDir(), name, relative, contents); |
| } |
| |
| protected File makeTestFile(File dir, String name, String relative, |
| final InputStream contents) throws IOException { |
| if (relative != null) { |
| dir = new File(dir, relative); |
| if (!dir.exists()) { |
| boolean mkdir = dir.mkdirs(); |
| assertTrue(dir.getPath(), mkdir); |
| } |
| } else if (!dir.exists()) { |
| boolean mkdir = dir.mkdirs(); |
| assertTrue(dir.getPath(), mkdir); |
| } |
| File tempFile = new File(dir, name); |
| if (tempFile.exists()) { |
| tempFile.delete(); |
| } |
| |
| Files.copy(new InputSupplier<InputStream>() { |
| @Override |
| public InputStream getInput() throws IOException { |
| return contents; |
| } |
| }, tempFile); |
| |
| return tempFile; |
| } |
| |
| /** |
| * Test file description, which can copy from resource directory or from |
| * a specified hardcoded string literal, and copy into a target directory |
| */ |
| public class TestFile { |
| public String sourceRelativePath; |
| public String targetRelativePath; |
| public String contents; |
| |
| public TestFile() { |
| } |
| |
| public TestFile withSource(@NonNull String source) { |
| contents = source; |
| return this; |
| } |
| |
| public TestFile from(@NonNull String from) { |
| sourceRelativePath = from; |
| return this; |
| } |
| |
| public TestFile to(@NonNull String to) { |
| targetRelativePath = to; |
| return this; |
| } |
| |
| public TestFile copy(@NonNull String relativePath) { |
| // Support replacing filenames and paths with a => syntax, e.g. |
| // dir/file.txt=>dir2/dir3/file2.java |
| // will read dir/file.txt from the test data and write it into the target |
| // directory as dir2/dir3/file2.java |
| String targetPath = relativePath; |
| int replaceIndex = relativePath.indexOf("=>"); //$NON-NLS-1$ |
| if (replaceIndex != -1) { |
| // foo=>bar |
| targetPath = relativePath.substring(replaceIndex + "=>".length()); |
| relativePath = relativePath.substring(0, replaceIndex); |
| } |
| sourceRelativePath = relativePath; |
| targetRelativePath = targetPath; |
| return this; |
| } |
| |
| @NonNull |
| public File createFile(@NonNull File targetDir) throws IOException { |
| InputStream stream; |
| if (contents != null) { |
| stream = new ByteArrayInputStream(contents.getBytes(Charsets.UTF_8)); |
| } else { |
| stream = getTestResource(sourceRelativePath, true); |
| } |
| assertNotNull(sourceRelativePath + " does not exist", stream); |
| int index = targetRelativePath.lastIndexOf('/'); |
| String relative = null; |
| String name = targetRelativePath; |
| if (index != -1) { |
| name = targetRelativePath.substring(index + 1); |
| relative = targetRelativePath.substring(0, index); |
| } |
| |
| return makeTestFile(targetDir, name, relative, stream); |
| } |
| } |
| |
| protected File getTestfile(File targetDir, String relativePath) throws IOException { |
| // Support replacing filenames and paths with a => syntax, e.g. |
| // dir/file.txt=>dir2/dir3/file2.java |
| // will read dir/file.txt from the test data and write it into the target |
| // directory as dir2/dir3/file2.java |
| |
| String targetPath = relativePath; |
| int replaceIndex = relativePath.indexOf("=>"); //$NON-NLS-1$ |
| if (replaceIndex != -1) { |
| // foo=>bar |
| targetPath = relativePath.substring(replaceIndex + "=>".length()); |
| relativePath = relativePath.substring(0, replaceIndex); |
| } |
| |
| InputStream stream = getTestResource(relativePath, true); |
| assertNotNull(relativePath + " does not exist", stream); |
| int index = targetPath.lastIndexOf('/'); |
| String relative = null; |
| String name = targetPath; |
| if (index != -1) { |
| name = targetPath.substring(index + 1); |
| relative = targetPath.substring(0, index); |
| } |
| |
| return makeTestFile(targetDir, name, relative, stream); |
| } |
| |
| protected static void addCleanupDir(File dir) { |
| sCleanDirs.add(dir); |
| try { |
| sCleanDirs.add(dir.getCanonicalFile()); |
| } catch (IOException e) { |
| fail(e.getLocalizedMessage()); |
| } |
| sCleanDirs.add(dir.getAbsoluteFile()); |
| } |
| |
| protected String cleanup(String result) { |
| List<File> sorted = new ArrayList<File>(sCleanDirs); |
| // Process dirs in order such that we match longest substrings first |
| Collections.sort(sorted, new Comparator<File>() { |
| @Override |
| public int compare(File file1, File file2) { |
| String path1 = file1.getPath(); |
| String path2 = file2.getPath(); |
| int delta = path2.length() - path1.length(); |
| if (delta != 0) { |
| return delta; |
| } else { |
| return path1.compareTo(path2); |
| } |
| } |
| }); |
| |
| for (File dir : sorted) { |
| if (result.contains(dir.getPath())) { |
| result = result.replace(dir.getPath(), "/TESTROOT"); |
| } |
| } |
| |
| // The output typically contains a few directory/filenames. |
| // On Windows we need to change the separators to the unix-style |
| // forward slash to make the test as OS-agnostic as possible. |
| if (File.separatorChar != '/') { |
| result = result.replace(File.separatorChar, '/'); |
| } |
| |
| return result; |
| } |
| |
| /** Get the location to write missing golden files to */ |
| protected File findSrcDir() { |
| // Set $ANDROID_SRC to point to your git AOSP working tree |
| String rootPath = System.getenv("ANDROID_SRC"); |
| if (rootPath == null) { |
| String sdk = System.getenv("ADT_SDK_SOURCE_PATH"); |
| if (sdk != null) { |
| File root = new File(sdk); |
| if (root.exists()) { |
| return root.getName().equals("sdk") ? root.getParentFile() : root; |
| } |
| } |
| } else { |
| File root = new File(rootPath); |
| if (root.exists()) { |
| return root; |
| } |
| } |
| |
| return null; |
| } |
| |
| protected File findSrcRelativeDir(String relative) { |
| // Set $ANDROID_SRC to point to your git AOSP working tree |
| File root = findSrcDir(); |
| if (root != null) { |
| File testData = new File(root, relative.replace('/', File.separatorChar)); |
| if (testData.exists()) { |
| return testData; |
| } |
| } |
| |
| return null; |
| } |
| } |