/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.testrunner;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import com.facebook.buck.testutil.integration.ProjectWorkspace;
import com.facebook.buck.testutil.integration.ProjectWorkspace.ProcessResult;
import com.facebook.buck.testutil.integration.TemporaryPaths;
import com.facebook.buck.testutil.integration.TestDataHelper;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
/**
* Integration test to verify that timeouts are handled as expected by {@link JUnitRunner}.
*
* <p>Note that there is a quirk when running tests with threads and timeouts described at
* https://github.com/junit-team/junit/issues/686. This test verifies that Buck honors this behavior
* of JUnit with its custom {@link BuckBlockJUnit4ClassRunner}.
*
* <p>That said, this behavior of JUnit interacts badly with a default test timeout in {@code
* .buckconfig} because it requires adding {@link org.junit.rules.Timeout} to the handful of tests
* that exploit this behavior.
*
*/
public class TimeoutIntegrationTest {
private static final String PATH_TO_TIMEOUT_BEHAVIOR_TEST = "TimeoutChangesBehaviorTest.java";
@Rule public TemporaryPaths temporaryFolder = new TemporaryPaths();
@Test
public void testThatTimeoutsInTestsWorkAsExpected() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(this, "timeouts", temporaryFolder);
workspace.setUp();
// ExceedsAnnotationTimeoutTest should fail.
ProcessResult exceedsAnnotationTimeoutTestResult =
workspace.runBuckCommand("test", "//:ExceedsAnnotationTimeoutTest");
exceedsAnnotationTimeoutTestResult.assertTestFailure("Test should fail due to timeout");
assertThat(
exceedsAnnotationTimeoutTestResult.getStderr(),
containsString(
"FAILURE com.example.ExceedsAnnotationTimeoutTest testShouldFailDueToExpiredTimeout: "
+ "test timed out after 1000 milliseconds"));
// TimeoutChangesBehaviorTest should pass.
ProcessResult timeoutTestWithoutTimeout =
workspace.runBuckCommand("test", "//:TimeoutChangesBehaviorTest");
timeoutTestWithoutTimeout.assertSuccess();
// TimeoutChangesBehaviorTest with @Test(timeout) specified should fail.
// See https://github.com/junit-team/junit/issues/686 about why it fails.
modifyTimeoutInTestAnnotation(PATH_TO_TIMEOUT_BEHAVIOR_TEST, /* addTimeout */ true);
ProcessResult timeoutTestWithTimeoutOnAnnotation =
workspace.runBuckCommand("test", "//:TimeoutChangesBehaviorTest");
timeoutTestWithTimeoutOnAnnotation.assertTestFailure();
assertThat(
timeoutTestWithTimeoutOnAnnotation.getStderr(),
containsString(
"FAILURE com.example.TimeoutChangesBehaviorTest "
+ "testTimeoutDictatesTheSuccessOfThisTest: Database should have an open transaction "
+ "due to setUp()."));
// TimeoutChangesBehaviorTest with @Rule(Timeout) should pass.
modifyTimeoutInTestAnnotation(PATH_TO_TIMEOUT_BEHAVIOR_TEST, /* addTimeout */ false);
insertTimeoutRule(PATH_TO_TIMEOUT_BEHAVIOR_TEST);
ProcessResult timeoutTestWithTimeoutRule =
workspace.runBuckCommand("test", "//:TimeoutChangesBehaviorTest");
timeoutTestWithTimeoutRule.assertSuccess();
workspace.verify();
}
@Test
public void individualTestCanOverrideTheDefaultTestTimeout() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(
this, "overridden_timeouts", temporaryFolder);
workspace.setUp();
// The .buckconfig in that workspace sets the default timeout to 1000ms.
ProcessResult result = workspace.runBuckCommand("test", "//:test");
result.assertSuccess();
}
@Test
public void testThatTimeoutsDumpsThreadStacks() throws IOException {
ProjectWorkspace workspace =
TestDataHelper.createProjectWorkspaceForScenario(this, "timeouts", temporaryFolder);
workspace.setUp();
ProcessResult testResult = workspace.runBuckCommand("test", "//:SleepTest");
assertThat(
testResult.getStderr(),
containsString("at com.example.SleepTest.testSleepABunch(SleepTest.java:"));
}
/**
* Swaps all instances of {@code @Test} with {@code @Test(timeout = 10000)} in the specified Java
* file, as determined by the value of {@code addTimeout}.
*/
private void modifyTimeoutInTestAnnotation(String path, final boolean addTimeout)
throws IOException {
Function<String, String> transform =
line -> {
String original = addTimeout ? "@Test" : "@Test(timeout = 100000)";
String replacement = addTimeout ? "@Test(timeout = 100000)" : "@Test";
return line.replace(original, replacement) + '\n';
};
rewriteFileWithTransform(path, transform);
}
/**
* Inserts the following after the top-level class declaration:
* <pre>
* @org.junit.Rule
* public org.junit.rules.Timeout timeoutForTests = new org.junit.rules.Timeout(10000);
* </pre>
*/
private void insertTimeoutRule(String path) throws IOException {
Function<String, String> transform =
line -> {
if (line.startsWith("public class")) {
return line
+ "\n\n"
+ " @org.junit.Rule\n"
+ " public org.junit.rules.Timeout timeoutForTests = "
+ "new org.junit.rules.Timeout(10000);\n";
} else {
return line + '\n';
}
};
rewriteFileWithTransform(path, transform);
}
/**
* Finds the file at the specified path, transforms all of its lines using the specified {@code
* transform} parameter, and writes the transformed lines back to the path.
*/
private void rewriteFileWithTransform(String path, Function<String, String> transform)
throws IOException {
Path javaFile = temporaryFolder.getRoot().resolve(path);
List<String> lines = Files.readAllLines(javaFile, Charsets.UTF_8);
String java = Joiner.on("").join(Iterables.transform(lines, transform));
Files.write(javaFile, java.getBytes(Charsets.UTF_8));
}
}