blob: f85873fb47e79d33cc01fceb9cdde66fb8776fa6 [file] [log] [blame]
/*
* 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;
}
}