blob: f833078bf9ca8ef1a819543a4f40108eb23e5b7f [file] [log] [blame]
/*
* Copyright 2013-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.cli;
import static com.facebook.buck.testutil.integration.ProjectWorkspace.ProcessResult;
import static java.util.concurrent.Executors.callable;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeNoException;
import static org.junit.Assume.assumeNotNull;
import static org.junit.Assume.assumeTrue;
import com.facebook.buck.android.AssumeAndroidPlatform;
import com.facebook.buck.android.FakeAndroidDirectoryResolver;
import com.facebook.buck.event.BuckEventListener;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildId;
import com.facebook.buck.rules.FakeRepositoryFactory;
import com.facebook.buck.rules.TestRepositoryBuilder;
import com.facebook.buck.rules.TestRunEvent;
import com.facebook.buck.testutil.TestConsole;
import com.facebook.buck.testutil.integration.DebuggableTemporaryFolder;
import com.facebook.buck.testutil.integration.DelegatingInputStream;
import com.facebook.buck.testutil.integration.ProjectWorkspace;
import com.facebook.buck.testutil.integration.TestContext;
import com.facebook.buck.testutil.integration.TestDataHelper;
import com.facebook.buck.timing.FakeClock;
import com.facebook.buck.util.CapturingPrintStream;
import com.facebook.buck.util.ImmutableProcessExecutorParams;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.environment.Platform;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.martiansoftware.nailgun.NGClientListener;
import com.martiansoftware.nailgun.NGContext;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class DaemonIntegrationTest {
private static final int SUCCESS_EXIT_CODE = 0;
private ScheduledExecutorService executorService;
private static final Gson gson = new Gson();
@Rule
public DebuggableTemporaryFolder tmp = new DebuggableTemporaryFolder();
@Before
public void setUp() throws IOException, InterruptedException{
executorService = Executors.newScheduledThreadPool(5);
// We assume watchman has been installed and configured properly on the system, and that setting
// up the watch is successful.
try {
ProcessExecutor.Result result = new ProcessExecutor(new TestConsole()).launchAndExecute(
ImmutableProcessExecutorParams.builder()
.setCommand(ImmutableList.of("watchman", "watchzzz", tmp.getRootPath().toString()))
.build());
assumeTrue(result.getStdout().isPresent());
Map<String, Object> response = gson.<Map<String, Object>>fromJson(
result.getStdout().get(),
Map.class);
assumeNotNull(response);
assumeFalse(response.containsKey("error"));
} catch (IOException e) {
assumeNoException(e);
}
}
@After
public void tearDown() {
Thread.interrupted(); // Clear interrupted flag, if set.
executorService.shutdown();
Main.resetDaemon();
}
/**
* This verifies that when the user tries to run a read/write command, while another is already
* running, the second call will fail. Serializing command execution in this way avoids
* multiple threads accessing and corrupting the static state used by the Buck daemon and
* trampling over each others output.
*/
@Test
public void whenConcurrentCommandExecutedThenSecondCommandFails()
throws IOException, InterruptedException, ExecutionException {
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
Future<?> firstThread = executorService.schedule(
createRunnableCommand(SUCCESS_EXIT_CODE, "build", "//:sleep"),
0,
TimeUnit.MILLISECONDS);
Future<?> secondThread = executorService.schedule(
createRunnableCommand(Main.BUSY_EXIT_CODE, "build", "//:sleep"),
500L,
TimeUnit.MILLISECONDS);
firstThread.get();
secondThread.get();
}
/**
* Verifies that a client timeout will be detected by a Nailgun
* NGInputStream reading from a blocking heartbeat stream.
*/
@Test(expected = InterruptedException.class)
public void whenClientTimeoutDetectedThenMainThreadIsInterrupted()
throws InterruptedException, IOException {
final long timeoutMillis = 100;
final long intervalMillis = timeoutMillis * 2; // Interval > timeout to trigger disconnection.
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
// Build an NGContext connected to an NGInputStream reading from a stream that will timeout.
Thread.currentThread().setName("Test");
try (TestContext context = new TestContext(
ImmutableMap.copyOf(System.getenv()),
TestContext.createHeartBeatStream(intervalMillis),
timeoutMillis)) {
final Thread commandThread = Thread.currentThread();
context.addClientListener(
new NGClientListener() {
@Override
public void clientDisconnected() throws InterruptedException {
commandThread.interrupt();
}
});
Thread.sleep(1000);
fail("Should have been interrupted.");
}
}
/**
* This verifies that a client timeout will be detected by a Nailgun
* NGInputStream reading from an empty heartbeat stream and that the generated
* InterruptedException will cause command execution to fail after timeout.
*/
@Test
public void whenClientTimeoutDetectedThenBuildIsInterrupted()
throws InterruptedException, IOException {
// Sub process interruption not supported on Windows.
assumeTrue(Platform.detect() != Platform.WINDOWS);
final long timeoutMillis = 100;
final long intervalMillis = timeoutMillis * 2; // Interval > timeout to trigger disconnection.
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
// Build an NGContext connected to an NGInputStream reading from stream that will timeout.
try (TestContext context = new TestContext(
ImmutableMap.copyOf(System.getenv()),
TestContext.createHeartBeatStream(intervalMillis),
timeoutMillis)) {
ProcessResult result = workspace.runBuckdCommand(context, "build", "//:sleep");
result.assertFailure();
assertThat(result.getStderr(), containsString("InterruptedException"));
}
}
@Test
public void whenConcurrentReadOnlyCommandExecutedThenReadOnlyCommandSucceeds()
throws IOException, InterruptedException, ExecutionException {
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
Future<?> firstThread = executorService.schedule(
createRunnableCommand(SUCCESS_EXIT_CODE, "build", "//:sleep"), 0, TimeUnit.MILLISECONDS);
Future<?> secondThread = executorService.schedule(
createRunnableCommand(SUCCESS_EXIT_CODE, "targets"), 500L, TimeUnit.MILLISECONDS);
firstThread.get();
secondThread.get();
}
/**
* This verifies that multiple read only commands can be executed concurrently successfully.
*/
@Test
public void whenReadOnlyCommandsExecutedConcurrentlyThenAllSucceed()
throws IOException, InterruptedException, ExecutionException {
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
executorService.invokeAll(
ImmutableList.of(
createCallableCommand(SUCCESS_EXIT_CODE, "audit", "input", "//:sleep"),
createCallableCommand(SUCCESS_EXIT_CODE, "audit", "input", "//:sleep"),
createCallableCommand(SUCCESS_EXIT_CODE, "audit", "input", "//:sleep"),
createCallableCommand(SUCCESS_EXIT_CODE, "audit", "input", "//:sleep"),
createCallableCommand(SUCCESS_EXIT_CODE, "audit", "input", "//:sleep"))
);
}
private Runnable createRunnableCommand(final int expectedExitCode, final String ... args) {
return new Runnable() {
@Override
public void run() {
try {
Main main = new Main(new CapturingPrintStream(), new CapturingPrintStream());
int exitCode = main.tryRunMainWithExitCode(
new BuildId(),
tmp.getRootPath(),
Optional.<NGContext>of(new TestContext()),
ImmutableMap.copyOf(System.getenv()),
args);
assertEquals("Unexpected exit code.", expectedExitCode, exitCode);
} catch (IOException e) {
fail("Should not throw exception.");
throw Throwables.propagate(e);
} catch (InterruptedException e) {
fail("Should not throw exception.");
Thread.currentThread().interrupt();
}
}
};
}
private Callable<Object> createCallableCommand(int expectedExitCode, String ... args) {
return callable(createRunnableCommand(expectedExitCode, args));
}
/**
* This verifies that a client disconnection will be detected by a Nailgun
* NGInputStream reading from an empty heartbeat stream and that the generated
* InterruptedException will interrupt command execution causing it to fail.
*/
@Test
public void whenClientTimeoutDetectedThenTestIsInterrupted()
throws InterruptedException, IOException {
// Sub process interruption not supported on Windows.
assumeTrue(Platform.detect() != Platform.WINDOWS);
final long timeoutMillis = 100;
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
// Start with an input stream that sends heartbeats at a regular rate.
final DelegatingInputStream inputStream = new DelegatingInputStream(
TestContext.createHeartBeatStream(timeoutMillis / 10));
// Build an NGContext connected to an NGInputStream reading from stream that will timeout.
try (TestContext context = new TestContext(
ImmutableMap.copyOf(System.getenv()),
inputStream,
timeoutMillis)) {
BuckEventListener listener = new BuckEventListener() {
@Subscribe
@SuppressWarnings("unused")
public void onEvent(TestRunEvent.Started event) {
// When tests start running, make the heartbeat stream time out.
inputStream.setDelegate(TestContext.createHeartBeatStream(2 * timeoutMillis));
}
@Override
public void outputTrace(BuildId buildId) throws InterruptedException {
// do nothing
}
};
ProcessResult result = workspace.runBuckdCommand(context, listener, "test", "//:test");
result.assertFailure();
assertThat(result.getStderr(), containsString("InterruptedException"));
}
}
/**
* This verifies that a client timeout will be detected by a Nailgun
* NGInputStream reading from an empty heartbeat stream and that the generated
* InterruptedException will cause command execution to fail after timeout.
*/
@Test
public void whenClientDisconnectionDetectedThenBuildIsInterrupted()
throws InterruptedException, IOException {
// Sub process interruption not supported on Windows.
assumeTrue(Platform.detect() != Platform.WINDOWS);
final long timeoutMillis = 2000; // Stream timeout > test timeout.
final long disconnectMillis = 100; // Disconnect before test timeout.
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
// Build an NGContext connected to an NGInputStream reading from stream that will timeout.
try (TestContext context = new TestContext(
ImmutableMap.copyOf(System.getenv()),
TestContext.createDisconnectionStream(disconnectMillis),
timeoutMillis)) {
ProcessResult result = workspace.runBuckdCommand(context, "build", "//:sleep");
result.assertFailure();
assertThat(result.getStderr(), containsString("InterruptedException"));
}
}
/**
* This verifies that a client disconnection will be detected by a Nailgun
* NGInputStream reading from an empty heartbeat stream and that the generated
* InterruptedException will interrupt command execution causing it to fail.
*/
@Test
public void whenClientDisconnectionDetectedThenTestIsInterrupted()
throws InterruptedException, IOException {
// Sub process interruption not supported on Windows.
assumeTrue(Platform.detect() != Platform.WINDOWS);
final long timeoutMillis = 2000; // Stream timeout > test timeout.
final long disconnectMillis = 100; // Disconnect before test timeout.
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "exclusive_execution", tmp);
workspace.setUp();
// Start with an input stream that sends heartbeats at a regular rate.
final DelegatingInputStream inputStream = new DelegatingInputStream(
TestContext.createHeartBeatStream(timeoutMillis / 10));
// Build an NGContext connected to an NGInputStream reading from stream that will timeout.
try (TestContext context = new TestContext(
ImmutableMap.copyOf(System.getenv()),
inputStream,
timeoutMillis)) {
BuckEventListener listener = new BuckEventListener() {
@Subscribe
@SuppressWarnings("unused")
public void onEvent(TestRunEvent.Started event) {
// When tests start running, make the heartbeat stream simulate a disconnection.
inputStream.setDelegate(TestContext.createDisconnectionStream(disconnectMillis));
}
@Override
public void outputTrace(BuildId buildId) throws InterruptedException {
// do nothing
}
};
ProcessResult result = workspace.runBuckdCommand(context, listener, "test", "//:test");
result.assertFailure();
assertThat(result.getStderr(), containsString("InterruptedException"));
}
}
@Test
public void whenAppBuckFileRemovedThenRebuildFails()
throws IOException, InterruptedException {
AssumeAndroidPlatform.assumeSdkIsAvailable();
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "file_watching", tmp);
workspace.setUp();
ProcessResult result = workspace.runBuckdCommand("build", "app");
result.assertSuccess();
String fileName = "apps/myapp/BUCK";
assertTrue("Should delete BUCK file successfully", workspace.getFile(fileName).delete());
waitForChange(Paths.get(fileName));
workspace.runBuckdCommand("build", "app").assertFailure();
}
@Test
public void whenActivityBuckFileRemovedThenRebuildFails()
throws IOException, InterruptedException {
AssumeAndroidPlatform.assumeSdkIsAvailable();
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "file_watching", tmp);
workspace.setUp();
workspace.runBuckdCommand("build", "//java/com/example/activity:activity").assertSuccess();
String fileName = "java/com/example/activity/BUCK";
assertTrue("Should delete BUCK file successfully.", workspace.getFile(fileName).delete());
waitForChange(Paths.get(fileName));
workspace.runBuckdCommand("build", "//java/com/example/activity:activity").assertFailure();
}
@Test
public void whenSourceInputRemovedThenRebuildFails()
throws IOException, InterruptedException {
AssumeAndroidPlatform.assumeNdkIsAvailable();
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "file_watching", tmp);
workspace.setUp();
workspace.runBuckdCommand("build", "//java/com/example/activity:activity").assertSuccess();
String fileName = "java/com/example/activity/MyFirstActivity.java";
assertTrue("Should delete BUCK file successfully.", workspace.getFile(fileName).delete());
waitForChange(Paths.get(fileName));
try {
workspace.runBuckdCommand("build", "//java/com/example/activity:activity");
fail("Should have thrown HumanReadableException.");
} catch (java.lang.RuntimeException e) {
assertThat("Failure should have been due to file removal.", e.getMessage(),
containsString("MyFirstActivity.java"));
}
}
@Test
public void whenSourceInputInvalidatedThenRebuildFails()
throws IOException, InterruptedException {
AssumeAndroidPlatform.assumeSdkIsAvailable();
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "file_watching", tmp);
workspace.setUp();
workspace.runBuckdCommand("build", "//java/com/example/activity:activity").assertSuccess();
String fileName = "java/com/example/activity/MyFirstActivity.java";
Files.write("Some Illegal Java".getBytes(Charsets.US_ASCII), workspace.getFile(fileName));
waitForChange(Paths.get(fileName));
workspace.runBuckdCommand("build", "//java/com/example/activity:activity").assertFailure();
}
@Test
public void whenAppBuckFileInvalidatedThenRebuildFails()
throws IOException, InterruptedException {
AssumeAndroidPlatform.assumeSdkIsAvailable();
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "file_watching", tmp);
workspace.setUp();
workspace.runBuckdCommand("build", "app").assertSuccess();
String fileName = "apps/myapp/BUCK";
Files.write("Some Illegal Python".getBytes(Charsets.US_ASCII), workspace.getFile(fileName));
waitForChange(Paths.get(fileName));
ProcessResult result = workspace.runBuckdCommand("build", "app");
assertThat(
"Failure should be due to syntax error.",
result.getStderr(),
containsString("SyntaxError: invalid syntax"));
result.assertFailure();
}
@Test
public void whenBuckConfigChangesParserInvalidated()
throws IOException, InterruptedException {
ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot().toPath());
ObjectMapper objectMapper = new ObjectMapper();
Object daemon = Main.getDaemon(
new FakeRepositoryFactory().setRootRepoForTesting(
new TestRepositoryBuilder().setBuckConfig(
new FakeBuckConfig(
ImmutableMap.<String, Map<String, String>>builder()
.put("somesection", ImmutableMap.of("somename", "somevalue"))
.build()))
.setFilesystem(filesystem)
.build()),
new FakeClock(0),
objectMapper);
assertEquals(
"Daemon should not be replaced when config equal.", daemon,
Main.getDaemon(
new FakeRepositoryFactory().setRootRepoForTesting(
new TestRepositoryBuilder().setBuckConfig(
new FakeBuckConfig(
ImmutableMap.<String, Map<String, String>>builder()
.put("somesection", ImmutableMap.of("somename", "somevalue"))
.build()))
.setFilesystem(filesystem)
.build()),
new FakeClock(0),
objectMapper));
assertNotEquals(
"Daemon should be replaced when config not equal.", daemon,
Main.getDaemon(
new FakeRepositoryFactory().setRootRepoForTesting(
new TestRepositoryBuilder().setBuckConfig(
new FakeBuckConfig(
ImmutableMap.<String, Map<String, String>>builder()
.put("somesection", ImmutableMap.of("somename", "someothervalue"))
.build()))
.setFilesystem(filesystem)
.build()),
new FakeClock(0),
objectMapper));
}
@Test
public void whenBuckBuiltTwiceLogIsPresent()
throws IOException, InterruptedException {
AssumeAndroidPlatform.assumeSdkIsAvailable();
final ProjectWorkspace workspace = TestDataHelper.createProjectWorkspaceForScenario(
this, "file_watching", tmp);
workspace.setUp();
workspace.runBuckdCommand("build", "//java/com/example/activity:activity").assertSuccess();
File buildLogFile = workspace.getFile("buck-out/bin/build.log");
assertTrue(buildLogFile.isFile());
assertTrue(buildLogFile.delete());
ProcessResult rebuild =
workspace.runBuckdCommand("build", "//java/com/example/activity:activity");
rebuild.assertSuccess();
buildLogFile = workspace.getFile("buck-out/bin/build.log");
assertTrue(buildLogFile.isFile());
}
@Test
public void whenAndroidDirectoryResolverChangesParserInvalidated()
throws IOException, InterruptedException {
ProjectFilesystem filesystem = new ProjectFilesystem(tmp.getRoot().toPath());
ObjectMapper objectMapper = new ObjectMapper();
Object daemon = Main.getDaemon(
new FakeRepositoryFactory().setRootRepoForTesting(
new TestRepositoryBuilder()
.setAndroidDirectoryResolver(
new FakeAndroidDirectoryResolver(
Optional.<Path>absent(),
Optional.<Path>absent(),
Optional.of("something")))
.setFilesystem(filesystem)
.build()),
new FakeClock(0),
objectMapper);
assertNotEquals(
"Daemon should be replaced when not equal.", daemon,
Main.getDaemon(
new FakeRepositoryFactory().setRootRepoForTesting(
new TestRepositoryBuilder()
.setAndroidDirectoryResolver(
new FakeAndroidDirectoryResolver(
Optional.<Path>absent(),
Optional.<Path>absent(),
Optional.of("different")))
.setFilesystem(filesystem)
.build()),
new FakeClock(0),
objectMapper));
}
private void waitForChange(final Path path) throws IOException, InterruptedException {
class Watcher {
private Path path;
private boolean watchedChange = false;
public Watcher(Path path) {
this.path = path;
watchedChange = false;
}
public boolean watchedChange() {
return watchedChange;
}
@Subscribe
public synchronized void onEvent(WatchEvent<?> event) throws IOException {
if (path.equals(event.context()) || event.kind() == StandardWatchEventKinds.OVERFLOW) {
watchedChange = true;
}
}
}
Watcher watcher = new Watcher(path);
Main.registerFileWatcher(watcher);
while (!watcher.watchedChange()) {
Main.watchFilesystem();
}
}
}