blob: 012c336c13623f168db3a4381d5eee9d4ce35bc4 [file] [log] [blame]
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 static org.junit.Assert.assertTrue;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.concurrency.GuardedBy;
import com.android.utils.FileUtils;
import com.android.utils.PathUtils;
import com.google.common.base.Charsets;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import junit.framework.TestCase;
/**
* Utility methods to deal with loading the test data.
*/
public class TestUtils {
/** Default timeout for the {@link #eventually(Runnable)} check. */
private static final Duration DEFAULT_EVENTUALLY_TIMEOUT = Duration.ofSeconds(10);
/** Time to wait between checks to obtain the value of an eventually supplier. */
private static final long EVENTUALLY_CHECK_CYCLE_TIME_MS = 10;
@GuardedBy("TestUtils.class")
private static File workspaceRoot = null;
/**
* Returns Kotlin version that is used in new project templates and integration tests.
*
* <p>This version is determined based on the checked-in kotlin-plugin prebuilt, and should be
* in sync with the version in:
*
* <ul>
* <li>buildSrc/base/dependencies.properties
* <li>tools/base/third_party/BUILD (this is generated from dependencies.properties)
* <li>tools/base/build-system/integration-test/application/BUILD
* <li>tools/base/build-system/integration-test/databinding/BUILD.bazel
* <li>tools/adt/idea/android/BUILD
* <li>tools/base/.idea/libraries definition for kotlin-stdlib-jre8
* <li>tools/idea/.idea/libraries definition for kotlin-stdlib-jre8
* </ul>
*/
@NonNull
public static String getKotlinVersionForTests() {
try {
return Files.readLines(
getWorkspaceFile(
"prebuilts/tools/common/kotlin-plugin/Kotlin/kotlinc/build.txt"),
Charsets.UTF_8)
.get(0);
} catch (IOException ex) {
throw new IllegalStateException("Could not determine Kotlin plugin version", ex);
}
}
/**
* Returns a File for the subfolder of the test resource data.
*
* @deprecated Use {@link #getWorkspaceRoot()} or {@link #getWorkspaceFile(String)} instead.
*/
@Deprecated
@NonNull
public static File getRoot(String... names) {
File root = new File("src/test/resources/testData/");
for (String name : names) {
root = new File(root, name);
// Hack: The sdk-common tests are not configured properly; running tests
// works correctly from Gradle but not from within the IDE. The following
// hack works around this quirk:
if (!root.isDirectory() && !root.getPath().contains("sdk-common")) {
File r = new File("sdk-common", root.getPath()).getAbsoluteFile();
if (r.isDirectory()) {
root = r;
}
}
TestCase.assertTrue("Test folder '" + name + "' does not exist! "
+ "(Tip: Check unit test launch config pwd)",
root.isDirectory());
}
return root;
}
public static void deleteFile(File dir) {
if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
deleteFile(f);
}
}
} else if (dir.isFile()) {
assertTrue(dir.getPath(), dir.delete());
}
}
public static File createTempDirDeletedOnExit() {
final File tempDir = Files.createTempDir();
PathUtils.addRemovePathHook(tempDir.toPath());
return tempDir;
}
/**
* Returns the root of the entire Android Studio codebase.
*
* <p>From this path, you should be able to access any file in the workspace via its full path,
* e.g.
*
* <p>new File(TestUtils.getWorkspaceRoot(), "tools/adt/idea/android/testSrc"); new
* File(TestUtils.getWorkspaceRoot(), "prebuilts/studio/jdk");
*
* <p>If this method is called by code run via IntelliJ / Gradle, it will simply walk its
* ancestor tree looking for the WORKSPACE file at its root; if called from Bazel, it will
* simply return the runfiles directory (which should be a mirror of the WORKSPACE root except
* only populated with explicitly declared dependencies).
*
* <p>Instead of calling this directly, prefer calling {@link #getWorkspaceFile(String)} as it
* is more resilient to cross-platform testing.
*
* @throws IllegalStateException if the current directory of the test is not a subdirectory of
* the workspace directory when this method is called. This shouldn't happen if the test is
* run by Bazel or run by IntelliJ with default configuration settings (where the working
* directory is initialized to the module root).
*/
@NonNull
public static synchronized File getWorkspaceRoot() {
// The logic below depends on the current working directory, so we save the results and hope the first call
// is early enough for the user.dir property to be unchanged.
if (workspaceRoot == null) {
// If we are using Bazel (which defines the following env vars), simply use the sandboxed
// root they provide us.
String workspace = System.getenv("TEST_WORKSPACE");
String workspaceParent = System.getenv("TEST_SRCDIR");
File currDir = new File("");
if (workspace != null && workspaceParent != null) {
currDir = new File(workspaceParent, workspace);
workspaceRoot = currDir;
}
File initialDir = currDir;
// If we're using a non-Bazel build system. At this point, assume our working
// directory is located underneath our codebase's root folder, so keep navigating up until
// we find it. If we're using Bazel, we should still look to see if there's a larger
// outermost workspace since we might be within a nested workspace.
while (currDir != null) {
currDir = currDir.getAbsoluteFile();
if (new File(currDir, "WORKSPACE").exists()) {
workspaceRoot = currDir;
}
currDir = currDir.getParentFile();
}
if (workspaceRoot == null) {
throw new IllegalStateException(
"Could not find WORKSPACE root. Is the original working directory a "
+ "subdirectory of the Android Studio codebase?\n\n"
+ "pwd = "
+ initialDir.getAbsolutePath());
}
}
return workspaceRoot;
}
/**
* Given a full path to a file from the base of the current workspace, return the file.
*
* e.g.
* TestUtils.getWorkspaceFile("tools/adt/idea/android/testSrc");
* TestUtils.getWorkspaceFile("prebuilts/studio/jdk");
*
* This method guarantees the file exists, throwing an exception if not found, so tests can
* safely use the file immediately after receiving it.
*
* In order to have the same method call work on both Windows and non-Windows machines, if the
* current OS is Windows and the target path is found with a common windows extension on it,
* then it will automatically be returned, e.g. "/path/to/binary" -> "/path/to/binary.exe".
*
* @throws IllegalArgumentException if the path results in a file that's not found.
*/
@NonNull
public static File getWorkspaceFile(@NonNull String path) {
File f = new File(getWorkspaceRoot(), path);
if (!f.exists() && OsType.getHostOs() == OsType.WINDOWS) {
// This file may be a binary with a .exe extension
// TODO: Confirm this works on Windows
f = new File(f.getPath() + ".exe");
}
if (!f.exists()) {
throw new IllegalArgumentException("File \"" + path + "\" not found at \"" + getWorkspaceRoot() + "\"");
}
return f;
}
/**
* @return a directory which tests can output data to. If running through Bazel's test runner,
* this returns the directory as specified by the TEST_UNDECLARED_OUTPUT_DIR environment
* variable. Data written to this directory will be zipped and made available under the
* WORKSPACE_ROOT/bazel-testlogs/ after the test completes. For non-Bazel runs, this
* currently returns a tmp directory that is deleted on exit.
*/
@NonNull
public static File getTestOutputDir() {
// If running via bazel, returns the sandboxed test output dir.
String testOutputDir = System.getenv("TEST_UNDECLARED_OUTPUTS_DIR");
if (testOutputDir != null) {
return new File(testOutputDir);
}
return createTempDirDeletedOnExit();
}
/**
* Returns a file at {@code path} relative to the root for {@link #getLatestAndroidPlatform}.
*
* @throws IllegalStateException if the current OS is not supported.
* @throws IllegalArgumentException if the path results in a file not found.
*/
@NonNull
public static File getPlatformFile(String path) {
String latestAndroidPlatform = getLatestAndroidPlatform();
File file =
FileUtils.join(getSdk(), SdkConstants.FD_PLATFORMS, latestAndroidPlatform, path);
if (!file.exists()) {
throw new IllegalArgumentException(
"File \"" + path + "\" not found in platform " + latestAndroidPlatform);
}
return file;
}
/** Checks if tests were started by Bazel. */
public static boolean runningFromBazel() {
return System.getenv().containsKey("TEST_WORKSPACE");
}
/**
* Returns the SDK directory relative to the workspace.
*
* @throws IllegalStateException if the current OS is not supported.
* @throws IllegalArgumentException if the path results in a file not found.
* @return a valid File object pointing at the SDK directory.
*/
@NonNull
public static String getRelativeSdk() {
OsType osType = OsType.getHostOs();
if (osType == OsType.UNKNOWN) {
throw new IllegalStateException(
"SDK test not supported on unknown platform: " + OsType.getOsName());
}
String hostDir = osType.getFolderName();
return "prebuilts/studio/sdk/" + hostDir;
}
/**
* Returns the SDK directory.
*
* @throws IllegalStateException if the current OS is not supported.
* @throws IllegalArgumentException if the path results in a file not found.
* @return a valid File object pointing at the SDK directory.
*/
@NonNull
public static File getSdk() {
return getWorkspaceFile(getRelativeSdk());
}
/**
* Returns the path to checked-in NDK.
*
* @see #getSdk()
*/
@NonNull
public static File getNdk() {
return new File(getSdk(), SdkConstants.FD_NDK);
}
/**
* Returns the remote SDK directory.
*
* @throws IllegalArgumentException if the path results in a file not found.
* @return a valid File object pointing at the remote SDK directory.
*/
@NonNull
public static Path getRemoteSdk() {
return getWorkspaceFile("prebuilts/studio/sdk/remote/dl.google.com/android/repository")
.toPath();
}
@NonNull
public static String getLatestAndroidPlatform() {
return "android-26";
}
/**
* Sleeps the current thread for enough time to ensure that the local file system had enough
* time to notice a "tick". This method is usually called in tests when it is necessary to
* ensure filesystem writes are detected through timestamp modification.
*
* @throws InterruptedException waiting interrupted
* @throws IOException issues creating a temporary file
*/
public static void waitForFileSystemTick() throws InterruptedException, IOException {
waitForFileSystemTick(getFreshTimestamp());
}
/**
* Sleeps the current thread for enough time to ensure that the local file system had enough
* time to notice a "tick". This method is usually called in tests when it is necessary to
* ensure filesystem writes are detected through timestamp modification.
*
* @param currentTimestamp last timestamp read from disk
* @throws InterruptedException waiting interrupted
* @throws IOException issues creating a temporary file
*/
public static void waitForFileSystemTick(long currentTimestamp)
throws InterruptedException, IOException {
while (getFreshTimestamp() <= currentTimestamp) {
Thread.sleep(100);
}
}
private static long getFreshTimestamp() throws IOException {
File notUsed = File.createTempFile(TestUtils.class.getName(), "waitForFileSystemTick");
long freshTimestamp = notUsed.lastModified();
FileUtils.delete(notUsed);
return freshTimestamp;
}
@NonNull
public static String getDiff(@NonNull String before, @NonNull String after) {
return getDiff(before, after, 0);
}
@NonNull
public static String getDiff(@NonNull String before, @NonNull String after, int windowSize) {
return getDiff(before.split("\n"), after.split("\n"), windowSize);
}
@NonNull
public static String getDiff(@NonNull String[] before, @NonNull String[] after) {
return getDiff(before, after, 0);
}
public static String getDiff(@NonNull String[] before, @NonNull String[] after,
int windowSize) {
// 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');
if (windowSize > 0) {
for (int context = Math.max(0, i - windowSize); context < i; context++) {
sb.append(" ");
sb.append(before[context]);
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 (windowSize > 0) {
for (int context = i; context < Math.min(n, i + windowSize); context++) {
sb.append(" ");
sb.append(before[context]);
sb.append("\n");
}
}
}
}
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();
}
/**
* Asserts that a runnable will eventually not throw an assertion exception. Equivalent to
* {@link #eventually(Runnable, Duration)}, but using a default timeout
*
* @param runnable a description of the failure, if the condition never becomes {@code true}
*/
public static void eventually(@NonNull Runnable runnable) {
eventually(runnable, DEFAULT_EVENTUALLY_TIMEOUT);
}
/**
* Asserts that a runnable will eventually not throw {@link AssertionError} before
* {@code timeoutMs} milliseconds have ellapsed
*
* @param runnable a description of the failure, if the condition never becomes {@code true}
* @param duration the timeout for the predicate to become true
*/
public static void eventually(@NonNull Runnable runnable, Duration duration) {
AssertionError lastError = null;
Instant timeoutTime = Instant.now().plus(duration);
while (Instant.now().isBefore(timeoutTime)) {
try {
runnable.run();
return;
} catch (AssertionError e) {
/*
* It is OK to throw this. Save for later.
*/
lastError = e;
}
try {
Thread.sleep(EVENTUALLY_CHECK_CYCLE_TIME_MS);
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
throw new AssertionError(
"Timed out waiting for runnable not to throw; last error was:",
lastError);
}
/**
* Launches a new process to execute the class {@code toRun} main() method and blocks until the
* process exits.
*
* @param toRun the class whose main() method will be executed in a new process.
* @param args the arguments for the class {@code toRun} main() method
* @throws RuntimeException if any (checked or runtime) exception occurs or the process returns
* an exit value other than 0
*/
public static void launchProcess(@NonNull Class toRun, String... args) {
List<String> commandAndArgs = new LinkedList<>();
commandAndArgs.add(FileUtils.join(System.getProperty("java.home"), "bin", "java"));
commandAndArgs.add("-cp");
commandAndArgs.add(System.getProperty("java.class.path"));
commandAndArgs.add(toRun.getName());
commandAndArgs.addAll(Arrays.asList(args));
ProcessBuilder processBuilder = new ProcessBuilder(commandAndArgs);
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
Process process;
try {
process = processBuilder.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
process.waitFor();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (process.exitValue() != 0) {
throw new RuntimeException(
"Process returned non-zero exit value: " + process.exitValue());
}
}
}